From a7de767122dc33bf547f91979fc4c997dfdfd6c9 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:42:07 +0000 Subject: [PATCH 01/33] chore(stack): bump storage to v1.84.20-r0 (#7298) This bumps storage from v1.84.19-r0 to v1.84.20-r0. https://github.com/supabase/slim-services/releases/tag/storage-v1.84.20-r0 Planned after supabase/slim-services published v1.84.20-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 74b0373f1d..ca8f2ec08f 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.147.1 AS realtime -FROM supabase/storage-api:v1.84.19 AS storage +FROM supabase/storage-api:v1.84.20 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 74b0373f1d..ca8f2ec08f 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.147.1 AS realtime -FROM supabase/storage-api:v1.84.19 AS storage +FROM supabase/storage-api:v1.84.20 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 1869d7ad08..f2c0b8f792 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -343,23 +343,23 @@ const definitions: Readonly> = { storage: definition( "storage", { - upstreamVersion: "v1.84.19", + upstreamVersion: "v1.84.20", revision: 0, image: - "ghcr.io/supabase/cli/storage:v1.84.19-r0@sha256:ac022dd6fba7ef5f84d676bbe776445c5e1955b786fe14b75a5669015ea771f2", - upstreamImage: "supabase/storage-api:v1.84.19", + "ghcr.io/supabase/cli/storage:v1.84.20-r0@sha256:d876648995c45980642f9405590c97085ea7144838b6362b83a7b79a9527cdf7", + upstreamImage: "supabase/storage-api:v1.84.20", natives: { "darwin-arm64": { - archive: "019aa9887ffeddf03a1465ae3a59eaaa983a09aecd75b1ad1371aa40cd72f2ea", - manifest: "73395585a635aad109912ae78206f68939c7c927cc2b69db9923f15a7804dfa2", + archive: "681201b513e061bd705a61d71360441c75734cd3036a5595347074c5edb137e0", + manifest: "0b61e7005015b0fe1197c84e3501721df412296e9205200c12fe3bc11fe151b2", }, "linux-amd64": { - archive: "61c049488a4deec67a8b4da947057924391f7efa0e7717b0648ca170255ee998", - manifest: "77f281f5684dd16df93162fb38740ade98464295902247403f2f8607bd5d9320", + archive: "59bd88da452b495f8f518bc316ad3b0c86a21228fa2f8d7827e3b0155f35b2b4", + manifest: "1c70eb3e72a0fb280fb0974c09cc5b844e88eae75d570dacbcb2bf3a09579d2e", }, "linux-arm64": { - archive: "689340dcd56b740335a144bf9b3ddb1146e3f13973d0a593df565d22e8ce151a", - manifest: "7d9e3b1d4aa8f91c2707641b64d44d8efe32a61d2923e0c0940729193199fef9", + archive: "d78f253d96d7bb9fb3965b61c38d94b1f525a9f727f5856dc561a64619643055", + manifest: "57b255ae94208a91b36859b395b4b5fc154e3edaf1a14e8f059be837069ab85c", }, }, }, From 2bdf6c570e7b72b86795eb3155571accbc038b10 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:52:40 +0000 Subject: [PATCH 02/33] chore(stack): bump storage to v1.84.21-r0 (#7303) This bumps storage from v1.84.20-r0 to v1.84.21-r0. https://github.com/supabase/slim-services/releases/tag/storage-v1.84.21-r0 Planned after supabase/slim-services published v1.84.21-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index ca8f2ec08f..0446e47bdf 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.147.1 AS realtime -FROM supabase/storage-api:v1.84.20 AS storage +FROM supabase/storage-api:v1.84.21 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index ca8f2ec08f..0446e47bdf 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.147.1 AS realtime -FROM supabase/storage-api:v1.84.20 AS storage +FROM supabase/storage-api:v1.84.21 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index f2c0b8f792..b40b1aa6d4 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -343,23 +343,23 @@ const definitions: Readonly> = { storage: definition( "storage", { - upstreamVersion: "v1.84.20", + upstreamVersion: "v1.84.21", revision: 0, image: - "ghcr.io/supabase/cli/storage:v1.84.20-r0@sha256:d876648995c45980642f9405590c97085ea7144838b6362b83a7b79a9527cdf7", - upstreamImage: "supabase/storage-api:v1.84.20", + "ghcr.io/supabase/cli/storage:v1.84.21-r0@sha256:8e77b412dd97a53b9e88beab2497bbf6899bbce3f3e593508e2a145cf3eb913a", + upstreamImage: "supabase/storage-api:v1.84.21", natives: { "darwin-arm64": { - archive: "681201b513e061bd705a61d71360441c75734cd3036a5595347074c5edb137e0", - manifest: "0b61e7005015b0fe1197c84e3501721df412296e9205200c12fe3bc11fe151b2", + archive: "9242f9311ab336ec1f82e91b15bd62a1b1d1ee6602b5e3337f6b314b5d5487d6", + manifest: "0917e94e57a726b345636a5f606b60132c9102f9d1a1f412d475fc9ac10a546c", }, "linux-amd64": { - archive: "59bd88da452b495f8f518bc316ad3b0c86a21228fa2f8d7827e3b0155f35b2b4", - manifest: "1c70eb3e72a0fb280fb0974c09cc5b844e88eae75d570dacbcb2bf3a09579d2e", + archive: "9a57bc5d5027c7a2bf7b995ac88168a590f599b2eca63047499ac7a1f2dfc232", + manifest: "45b4439c92c83377d03bf9e2bf3c28bf9d164329a8100573d52f80108a318806", }, "linux-arm64": { - archive: "d78f253d96d7bb9fb3965b61c38d94b1f525a9f727f5856dc561a64619643055", - manifest: "57b255ae94208a91b36859b395b4b5fc154e3edaf1a14e8f059be837069ab85c", + archive: "2b00c4c8ab62e3ca2508ae237ec3ead78152d5d1dbb68c0fa291977f3ed2e2ab", + manifest: "83aae8b24999bb5647673116a523ed4a5e75e103262b5fc4912e5ba4e0258671", }, }, }, From da27a34b7dbb281b1012a78fb8c0b1646c556058 Mon Sep 17 00:00:00 2001 From: kanad Date: Fri, 23 Oct 2026 14:40:29 +0000 Subject: [PATCH 03/33] ci: bump toolchains and upgrade mise-action to v5 (#7285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Toolchain bumps - Bumps pnpm, node, bun, and mise. `mise.lock` and `pnpm-lock.yaml` are updated to match. - Updates the mise version pinned in each GitHub Actions workflow. - Drops `pnpm exec` from `bun` commands. The toolchain setup already puts bun on `PATH`, so running it through pnpm only added a process hop and a pnpm lookup. Calling bun directly removes that overhead and complexity. ## Workflow changes ### flaky-check installs bun with `oven-sh/setup-bun` Its plan and report jobs only need bun. setup-bun restores a 34 MB cache with just the bun binary. mise-action restored a 68 MB mise data directory and then ran a locked install and a listing on top, which took about twice as long on a cache hit. Every other bun-only step in the repository already uses setup-bun, so this makes them consistent. ### `jdx/mise-action` moves from v4.3.0 to v5.2.2 What we gain: - **The mise binary is verified before it runs.** A downloaded binary is checked against the release's signed manifest, and a binary restored from cache is checked against the signed release checksums. Before, a cached binary could run unchecked. - **The job's GitHub token stays inside the action.** v4 wrote it to the environment of every later step. Now only the action sees it. - **Caches rebuild every 30 days.** GitHub never evicts a cache that keeps getting hits, so a busy repository could keep the same tools for months. The default key now rolls over on a schedule. What does not change for us: every step pins a mise version, so the new release-age default does not apply, and the pinned mise ships the signed manifest the action now requires. One thing to expect: the default cache key format changed, so each mise cache misses once after this lands. The sync check job runs `mise install` in a later step. It no longer gets the token, but it fetches release metadata through mise's own relay and downloads assets directly from GitHub releases, so it never used the token. ### The shared setup action drops its custom `cache_key` The custom key hashed `mise.toml`, `mise.lock`, `.node-version`, `.bun-version`, and `package.json`. mise-action's default key already covers `mise.toml` and `mise.lock`, plus the requested tool subset, the runner image, and the mise version. The lockfile pins the node, bun, and pnpm versions the version files select, and a locked install refuses any version the lockfile does not have. So the extra hashes could not catch anything the lockfile hash misses, and the default key is strictly better. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude --- .bun-version | 2 +- .github/actions/setup/action.yml | 5 +- .github/workflows/apply-release-notes.yml | 2 +- .github/workflows/build-cli-artifacts.yml | 4 +- .github/workflows/cli-go-ci.yml | 5 +- .github/workflows/flaky-check.yml | 14 +- .github/workflows/mirror-template-images.yml | 2 +- .github/workflows/propose-release-notes.yml | 2 +- .../publish-preview-cli-packages.yml | 2 +- .github/workflows/release-config.yml | 4 +- .github/workflows/release-shared.yml | 12 +- .github/workflows/sync-branches.yml | 4 +- apps/cli/docs/release-process.md | 6 +- apps/cli/package.json | 2 +- apps/cli/scripts/build.ts | 2 +- apps/cli/scripts/refresh-e2e-timings.ts | 2 +- apps/cli/scripts/sync-versions.ts | 2 +- apps/cli/scripts/upload-release-assets.ts | 2 +- ...1-cli-release-and-distribution-strategy.md | 4 +- mise.lock | 166 +++++++++--------- mise.toml | 2 +- package.json | 2 +- pnpm-lock.yaml | 124 ++++++------- 23 files changed, 182 insertions(+), 190 deletions(-) diff --git a/.bun-version b/.bun-version index 9df886c42a..1c99cf0e80 100644 --- a/.bun-version +++ b/.bun-version @@ -1 +1 @@ -1.4.2 +1.4.4 diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 8e503fe8f6..eebbb5d160 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -126,11 +126,10 @@ runs: commit: ${{ steps.sticky.outputs.commit }} - name: Install toolchains - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + uses: jdx/mise-action@8418fedb1110381a0d3ba51a7a415d7c1b884550 # v5.2.2 with: - version: 2026.9.0 + version: 2026.10.17 install_args: ${{ inputs.tools }} - cache_key: ${{ inputs.tools != '' && format('mise-{0}-{1}-{2}-{3}', runner.os, runner.arch, inputs.tools, hashFiles('mise.toml', 'mise.lock', '.node-version', '.bun-version', 'package.json')) || '' }} - name: Resolve pnpm store path if: inputs.dependency-cache == 'true' diff --git a/.github/workflows/apply-release-notes.yml b/.github/workflows/apply-release-notes.yml index b9858b3428..753028e5cc 100644 --- a/.github/workflows/apply-release-notes.yml +++ b/.github/workflows/apply-release-notes.yml @@ -99,7 +99,7 @@ jobs: exit 1 fi echo "==> Applying notes for $tag" - pnpm exec bun apps/cli/scripts/apply-release-notes.ts --tag "$tag" + bun apps/cli/scripts/apply-release-notes.ts --tag "$tag" release_url="https://github.com/${{ github.repository }}/releases/tag/${tag}" gh pr comment "$PR_NUMBER" --repo "${{ github.repository }}" --body \ "Applied to [${tag}](${release_url}) after approval by @${APPROVER}." diff --git a/.github/workflows/build-cli-artifacts.yml b/.github/workflows/build-cli-artifacts.yml index 2e8c2a32db..818f238b37 100644 --- a/.github/workflows/build-cli-artifacts.yml +++ b/.github/workflows/build-cli-artifacts.yml @@ -104,11 +104,11 @@ jobs: rcodesign --version - name: Sync versions - run: pnpm exec bun apps/cli/scripts/sync-versions.ts --version "${VERSION}" + run: bun apps/cli/scripts/sync-versions.ts --version "${VERSION}" - name: Build CLI artifacts timeout-minutes: ${{ inputs.build_timeout_minutes }} - run: pnpm exec bun apps/cli/scripts/build.ts --version "${VERSION}" + run: bun apps/cli/scripts/build.ts --version "${VERSION}" - name: Verify build artifacts run: | diff --git a/.github/workflows/cli-go-ci.yml b/.github/workflows/cli-go-ci.yml index a4e60bfab4..2ae77eac79 100644 --- a/.github/workflows/cli-go-ci.yml +++ b/.github/workflows/cli-go-ci.yml @@ -91,10 +91,9 @@ jobs: with: persist-credentials: false - - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + - uses: jdx/mise-action@8418fedb1110381a0d3ba51a7a415d7c1b884550 # v5.2.2 with: - version: 2026.9.0 - install: true + version: 2026.10.17 install_args: >- go golangci-lint diff --git a/.github/workflows/flaky-check.yml b/.github/workflows/flaky-check.yml index 73a547d9a1..54c9bdfbfc 100644 --- a/.github/workflows/flaky-check.yml +++ b/.github/workflows/flaky-check.yml @@ -76,12 +76,9 @@ jobs: persist-credentials: false - name: Install Bun - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - version: 2026.9.0 - install: true - install_args: bun - working_directory: .flaky-tooling + bun-version-file: .flaky-tooling/.bun-version - name: Plan runs id: plan @@ -204,12 +201,9 @@ jobs: persist-credentials: false - name: Install Bun - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - version: 2026.9.0 - install: true - install_args: bun - working_directory: .flaky-tooling + bun-version-file: .flaky-tooling/.bun-version - name: Download results uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/mirror-template-images.yml b/.github/workflows/mirror-template-images.yml index dabc95430e..1b45c64493 100644 --- a/.github/workflows/mirror-template-images.yml +++ b/.github/workflows/mirror-template-images.yml @@ -66,7 +66,7 @@ jobs: # are skipped, so a re-run produces an empty list. - name: Detect images missing from the mirror id: detect - run: pnpm exec bun apps/cli/scripts/detect-unmirrored-images.ts + run: bun apps/cli/scripts/detect-unmirrored-images.ts mirror: name: Mirror image diff --git a/.github/workflows/propose-release-notes.yml b/.github/workflows/propose-release-notes.yml index c459fdcf8c..04f9359071 100644 --- a/.github/workflows/propose-release-notes.yml +++ b/.github/workflows/propose-release-notes.yml @@ -82,4 +82,4 @@ jobs: - name: Propose release notes env: GH_TOKEN: ${{ steps.app-token.outputs.token }} - run: pnpm exec bun apps/cli/scripts/propose-release-notes.ts --tag "${TAG}" --apply + run: bun apps/cli/scripts/propose-release-notes.ts --tag "${TAG}" --apply diff --git a/.github/workflows/publish-preview-cli-packages.yml b/.github/workflows/publish-preview-cli-packages.yml index 73e2477257..9f5ce2057f 100644 --- a/.github/workflows/publish-preview-cli-packages.yml +++ b/.github/workflows/publish-preview-cli-packages.yml @@ -82,7 +82,7 @@ jobs: - name: Prepare package files run: | set -euo pipefail - pnpm exec bun apps/cli/scripts/sync-versions.ts --version "${PREVIEW_VERSION}" + bun apps/cli/scripts/sync-versions.ts --version "${PREVIEW_VERSION}" pnpm --dir apps/cli build:shim find packages -path '*/bin/supabase*' -type f -exec chmod +x {} + diff --git a/.github/workflows/release-config.yml b/.github/workflows/release-config.yml index 3ab2d2ac0a..2b79a91614 100644 --- a/.github/workflows/release-config.yml +++ b/.github/workflows/release-config.yml @@ -91,7 +91,7 @@ jobs: resume_token="$(openssl rand -hex 16)" echo "::stop-commands::${resume_token}" trap 'echo "::${resume_token}::"' EXIT - pnpm exec bun packages/config/scripts/release-plan.ts --notes-out "$RUNNER_TEMP/config-release-notes.md" + bun packages/config/scripts/release-plan.ts --notes-out "$RUNNER_TEMP/config-release-notes.md" # Build/gate/pack also run on private-blocked pushes (should_release=false, version set): # if `private` is ever flipped back on, the plan half of the release train is already @@ -127,7 +127,7 @@ jobs: mkdir -p "$RUNNER_TEMP/config-release-gate-local" tar -xzf "$RUNNER_TEMP/config-release-artifact/supabase-config-${VERSION}.tgz" \ -C "$RUNNER_TEMP/config-release-gate-local" --no-same-owner --no-same-permissions - pnpm exec bun tools/config-release-gate.ts --version "$VERSION" \ + bun tools/config-release-gate.ts --version "$VERSION" \ --local-dist "$RUNNER_TEMP/config-release-gate-local/package/dist" - name: Upload release artifact diff --git a/.github/workflows/release-shared.yml b/.github/workflows/release-shared.yml index 50f3f57213..1f0e0a13c9 100644 --- a/.github/workflows/release-shared.yml +++ b/.github/workflows/release-shared.yml @@ -318,10 +318,10 @@ jobs: fail-on-cache-miss: true - name: Sync versions - run: pnpm exec bun apps/cli/scripts/sync-versions.ts --version "${VERSION}" + run: bun apps/cli/scripts/sync-versions.ts --version "${VERSION}" - name: Publish to npm - run: pnpm exec bun apps/cli/scripts/publish.ts --tag "${NPM_TAG}" + run: bun apps/cli/scripts/publish.ts --tag "${NPM_TAG}" - name: Configure git for release pushes run: | @@ -389,13 +389,13 @@ jobs: - name: Upload release assets env: GH_TOKEN: ${{ github.token }} - run: pnpm exec bun apps/cli/scripts/upload-release-assets.ts upload --version "${VERSION}" + run: bun apps/cli/scripts/upload-release-assets.ts upload --version "${VERSION}" # Publishing makes the release immutable, so require every expected asset to be uploaded first. - name: Verify release assets env: GH_TOKEN: ${{ github.token }} - run: pnpm exec bun apps/cli/scripts/upload-release-assets.ts verify --version "${VERSION}" + run: bun apps/cli/scripts/upload-release-assets.ts verify --version "${VERSION}" - name: Publish GitHub Release (immutable) env: @@ -514,7 +514,7 @@ jobs: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Update Homebrew formula - run: pnpm exec bun apps/cli/scripts/update-homebrew.ts --version "${VERSION}" --name "${BREW_NAME}" + run: bun apps/cli/scripts/update-homebrew.ts --version "${VERSION}" --name "${BREW_NAME}" env: GH_TOKEN: ${{ steps.app-token.outputs.token }} GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} @@ -575,7 +575,7 @@ jobs: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Update Scoop manifest - run: pnpm exec bun apps/cli/scripts/update-scoop.ts --version "${VERSION}" --name "${SCOOP_NAME}" + run: bun apps/cli/scripts/update-scoop.ts --version "${VERSION}" --name "${SCOOP_NAME}" env: GH_TOKEN: ${{ steps.app-token.outputs.token }} GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/sync-branches.yml b/.github/workflows/sync-branches.yml index 5058c163f2..5eefcfbae3 100644 --- a/.github/workflows/sync-branches.yml +++ b/.github/workflows/sync-branches.yml @@ -198,9 +198,9 @@ jobs: bun-version-file: trusted/.bun-version # Installs mise only; the script installs the merged tree's own toolchain. - - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + - uses: jdx/mise-action@8418fedb1110381a0d3ba51a7a415d7c1b884550 # v5.2.2 with: - version: 2026.9.0 + version: 2026.10.17 install: false working_directory: work diff --git a/apps/cli/docs/release-process.md b/apps/cli/docs/release-process.md index 5b949f3ef9..de73842ecb 100644 --- a/apps/cli/docs/release-process.md +++ b/apps/cli/docs/release-process.md @@ -497,8 +497,8 @@ Keep two to three stable majors supported at the same time, roughly six to twelv `**build` (ubuntu-latest):\*\* -1. `[pnpm exec bun apps/cli/scripts/sync-versions.ts --version X.Y.Z](../scripts/sync-versions.ts)` — writes the release version into every `package.json` (umbrella + eight platform packages) and resolves the umbrella's `workspace:`\* `optionalDependencies` to `X.Y.Z`. -2. `[pnpm exec bun apps/cli/scripts/build.ts --version X.Y.Z](../scripts/build.ts)` — cross-compiles the Bun SFE for all eight targets (including windows-arm64), cross-compiles the Go sidecar, **ad-hoc signs the macOS binaries** (see [Code signing (macOS)](#code-signing-macos)), builds the six Linux packages via `nfpm`, produces the tar/zip archives, and writes `dist/checksums.txt`. +1. `[bun apps/cli/scripts/sync-versions.ts --version X.Y.Z](../scripts/sync-versions.ts)` — writes the release version into every `package.json` (umbrella + eight platform packages) and resolves the umbrella's `workspace:`\* `optionalDependencies` to `X.Y.Z`. +2. `[bun apps/cli/scripts/build.ts --version X.Y.Z](../scripts/build.ts)` — cross-compiles the Bun SFE for all eight targets (including windows-arm64), cross-compiles the Go sidecar, **ad-hoc signs the macOS binaries** (see [Code signing (macOS)](#code-signing-macos)), builds the six Linux packages via `nfpm`, produces the tar/zip archives, and writes `dist/checksums.txt`. 3. `actions/upload-artifact` preserves `packages/cli-*/bin/` and `dist/` for the downstream jobs. `**smoke-test` (matrix: `ubuntu-latest`, `macos-latest`, `macos-15-intel`, `windows-latest`):\*\* @@ -510,7 +510,7 @@ The matrix does not yet include `windows-11-arm` (gate 6) or an Alpine musl runn `**publish` (ubuntu-latest, `if: !inputs.dry_run`):\*\* 1. Re-runs `sync-versions.ts` (download-artifact restores file modes but not JSON mutations). -2. `[pnpm exec bun apps/cli/scripts/publish.ts --tag .stable>](../scripts/publish.ts)` — publishes the eight platform packages in parallel via OIDC trusted publishing (`bun publish --provenance`, no `NPM_TOKEN`), then the umbrella package last so `optionalDependencies` resolve cleanly at install time. +2. `[bun apps/cli/scripts/publish.ts --tag .stable>](../scripts/publish.ts)` — publishes the eight platform packages in parallel via OIDC trusted publishing (`bun publish --provenance`, no `NPM_TOKEN`), then the umbrella package last so `optionalDependencies` resolve cleanly at install time. 3. `softprops/action-gh-release` creates an **empty draft** Release `v` on `supabase/cli`. 4. `[upload-release-assets.ts upload](../scripts/upload-release-assets.ts)` uploads the archives, packages, `checksums.txt`, unversioned aliases, and `install` script **one asset at a time** with `gh release upload --clobber`, up to three attempts each with a five-minute timeout. `gh` itself retries 5xx responses and dropped connections three times within a second; the outer loop covers longer stalls and the `--clobber` delete, which `gh` does not retry. `uploads.github.com` fails single uploads often enough that this is routine: in September 2026 one beta release hit a multi-hour backend degradation and another lost one connection on a healthy backend. The asset list, retry policy, and timeout handling are covered by the unit and integration tests next to the script. 5. `upload-release-assets.ts verify` compares `gh release view --json assets` with the expected asset names. The job fails if any asset is missing or not `uploaded`, so a partial set is never published. diff --git a/apps/cli/package.json b/apps/cli/package.json index a9a6612162..9b1810b186 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -29,7 +29,7 @@ "build:binary": "bun scripts/build-binary.ts", "build:shim": "bun build src/shared/cli/bin.ts --outfile dist/supabase.js --target node", "docs:spec": "bun scripts/generate-docs-spec.ts", - "dev": "pnpm exec bun src/main.ts", + "dev": "bun src/main.ts", "gen:feedback-types": "pnpm dev -- gen types typescript --project-id xguihxuzqibwxjnimxev --schema public > src/shared/feedback/database.types.ts", "test": "pnpm run test:unit --coverage.enabled && pnpm run test:integration --coverage.enabled && pnpm run test:e2e", "test:unit": "pnpm exec turbo run supabase#test:unit:run --", diff --git a/apps/cli/scripts/build.ts b/apps/cli/scripts/build.ts index 52ee98efff..3d7f9db378 100644 --- a/apps/cli/scripts/build.ts +++ b/apps/cli/scripts/build.ts @@ -35,7 +35,7 @@ const packageJsonPath = path.join(root, "apps/cli/package.json"); const packageVersion = JSON.parse(await readFile(packageJsonPath, "utf8")) as { version?: string }; const version = values.version ?? packageVersion.version; if (!version) { - console.error("Usage: pnpm exec bun apps/cli/scripts/build.ts [--version ]"); + console.error("Usage: bun apps/cli/scripts/build.ts [--version ]"); process.exit(1); } if (values.version === undefined) { diff --git a/apps/cli/scripts/refresh-e2e-timings.ts b/apps/cli/scripts/refresh-e2e-timings.ts index a52667501c..1fbf485426 100644 --- a/apps/cli/scripts/refresh-e2e-timings.ts +++ b/apps/cli/scripts/refresh-e2e-timings.ts @@ -109,7 +109,7 @@ function readResultsCaches(dir: string): VitestResultsCache[] { }); } -const usage = `Usage: pnpm exec bun apps/cli/scripts/refresh-e2e-timings.ts --run [--repo supabase/cli] +const usage = `Usage: bun apps/cli/scripts/refresh-e2e-timings.ts --run [--repo supabase/cli] Downloads the e2e-timings-* artifacts of a test workflow run and rewrites apps/cli/tests/e2e-timings.json from them.`; diff --git a/apps/cli/scripts/sync-versions.ts b/apps/cli/scripts/sync-versions.ts index c94429bc4c..1f4cfe4fb5 100644 --- a/apps/cli/scripts/sync-versions.ts +++ b/apps/cli/scripts/sync-versions.ts @@ -24,7 +24,7 @@ const { values } = parseArgs({ const version = values.version; if (!version) { - console.error("Usage: pnpm exec bun apps/cli/scripts/sync-versions.ts --version "); + console.error("Usage: bun apps/cli/scripts/sync-versions.ts --version "); process.exit(1); } diff --git a/apps/cli/scripts/upload-release-assets.ts b/apps/cli/scripts/upload-release-assets.ts index d0edef1a35..a0138e12ed 100644 --- a/apps/cli/scripts/upload-release-assets.ts +++ b/apps/cli/scripts/upload-release-assets.ts @@ -153,7 +153,7 @@ export function missingAssets(expected: string[], uploaded: string[]): string[] return expected.filter((name) => !present.has(name)).sort(); } -const usage = `Usage: pnpm exec bun apps/cli/scripts/upload-release-assets.ts --version +const usage = `Usage: bun apps/cli/scripts/upload-release-assets.ts --version upload Upload every release asset to the draft release v, one at a time with retries. verify Fail unless every expected asset is present on v and reported as uploaded.`; diff --git a/docs/adr/0011-cli-release-and-distribution-strategy.md b/docs/adr/0011-cli-release-and-distribution-strategy.md index 1f04627fbd..80b55ec853 100644 --- a/docs/adr/0011-cli-release-and-distribution-strategy.md +++ b/docs/adr/0011-cli-release-and-distribution-strategy.md @@ -208,8 +208,8 @@ Draft, upload, verify, and finalize by the shared workflow: tag_name: v${{ inputs.version }} draft: true prerelease: ${{ inputs.prerelease }} -- run: pnpm exec bun apps/cli/scripts/upload-release-assets.ts upload --version "${VERSION}" -- run: pnpm exec bun apps/cli/scripts/upload-release-assets.ts verify --version "${VERSION}" +- run: bun apps/cli/scripts/upload-release-assets.ts upload --version "${VERSION}" +- run: bun apps/cli/scripts/upload-release-assets.ts verify --version "${VERSION}" - run: gh release edit v${VERSION} --draft=false ``` diff --git a/mise.lock b/mise.lock index 1f30d585a7..83da59b571 100644 --- a/mise.lock +++ b/mise.lock @@ -1,55 +1,55 @@ # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html -lockfile_version = 1 +lockfile_version = 3 [[tools.bun]] -version = "1.4.2" +version = "1.4.4" backend = "core:bun" -specifiers = ["1.4.2"] +specifiers = ["1.4.4"] [tools.bun."platforms.linux-arm64"] -checksum = "sha256:54328bbc2d9c8e0c9f892c544d66c57a83b84139e34909e5ee81758f1ac8fda7" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-aarch64.zip" +checksum = "sha256:baa6bb36f51d6e58561e02bbea825038a9df454f673d6d776e8bbf86f8c7850e" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-linux-aarch64.zip" [tools.bun."platforms.linux-arm64-musl"] -checksum = "sha256:71760b6c8ea30623b81a4907cb815d48e2ea266f2e73e751534a44a0607950df" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-aarch64-musl.zip" +checksum = "sha256:57f01b02ac7113c61c935f28f38c2eab849e3c970dab21f8b640a87b1ef65009" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-linux-aarch64-musl.zip" [tools.bun."platforms.linux-x64"] -checksum = "sha256:36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64.zip" +checksum = "sha256:bf8ccbfb2b0b80159e0f9a6d15c55317add8077534dbc6130a3edb52fccb395c" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-linux-x64.zip" [tools.bun."platforms.linux-x64-baseline"] -checksum = "sha256:c678040f14fe0440eb839d37cbd0ce4c051a32da72806ac97de6a6aab6bf728f" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64-baseline.zip" +checksum = "sha256:e0446eb55995772eb9894bebae9a6be368d985a8b9271ee124486f9946eedd76" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-linux-x64-baseline.zip" [tools.bun."platforms.linux-x64-musl"] -checksum = "sha256:4835eca59d6da70f4674f5642f6e459dcadab773695b2ed9922d131057989742" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64-musl.zip" +checksum = "sha256:a5a8ccc5e74dcb883fe974f125a7bdb3b41e5195ed1b870e73f17effd3eb2b2d" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-linux-x64-musl.zip" [tools.bun."platforms.linux-x64-musl-baseline"] -checksum = "sha256:76e1db84e98f22f78de0a87e309bfbbf297732847f9720db36750646c85c8c18" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-linux-x64-musl-baseline.zip" +checksum = "sha256:6e9c37098b293920aed28bfb51ab240e26d23bfdd9bbfecb3ad428ac166549bc" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-linux-x64-musl-baseline.zip" [tools.bun."platforms.macos-arm64"] -checksum = "sha256:90987a3a16d7db556d886ac3d551e7b6d3edf0a1cf43acaed622e8676be1d12f" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-darwin-aarch64.zip" +checksum = "sha256:c240d7f0ccb90c263a849fcdb09b7ee62630d921348ed6aafda86b9d577bc510" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-darwin-aarch64.zip" [tools.bun."platforms.macos-x64"] -checksum = "sha256:80520d7e17526308c9185d261679ac6d27798d3803a0e9f7ff9121ab8affb012" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-darwin-x64.zip" +checksum = "sha256:50a3491f73abc2fe81673171a85762d3bc2cc1680ccecadd983de99adf77977e" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-darwin-x64.zip" [tools.bun."platforms.macos-x64-baseline"] -checksum = "sha256:bad5bbd6cf14d0980d115f5954c9ff904df619d5e994d2da1ffccd3f316300b0" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-darwin-x64-baseline.zip" +checksum = "sha256:d4a155adfbfd09155eb3ba05c9e3a544f39297d7a68616d3c7759f67ab5c47dc" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-darwin-x64-baseline.zip" [tools.bun."platforms.windows-x64"] -checksum = "sha256:ce4c17497b2f29712a99d3d53f028de28cd42e3bacb8589599e7f000e49b6405" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-windows-x64.zip" +checksum = "sha256:cd2c0fccc78f3ede1f50ca827a15f063eda13d4d150a3d0674f4f6e323d864a5" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-windows-x64.zip" [tools.bun."platforms.windows-x64-baseline"] -checksum = "sha256:78c221c2376f79731ccf4e4af0b3bb46d81fefa3296c5abee09ad8a1b21e68c6" -url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.2/bun-windows-x64-baseline.zip" +checksum = "sha256:0f20ce6674d4f2dbcc8c5d59fdf99f7ba45f8c3b367e0f5781cbfa30a1c78d99" +url = "https://github.com/oven-sh/bun/releases/download/bun-v1.4.4/bun-windows-x64-baseline.zip" [[tools.go]] version = "1.26.5" @@ -243,123 +243,123 @@ url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/61133861 provenance = "github-attestations" [[tools.node]] -version = "24.21.0" +version = "24.22.1" backend = "core:node" specifiers = ["24"] [tools.node."platforms.linux-arm64"] -checksum = "sha256:724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5" -url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.gz" +checksum = "sha256:33c37be234ba8bee041961047b19160f4d7d0f164e8e246f78bdd10feff148b3" +url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-arm64.tar.gz" [tools.node."platforms.linux-arm64-musl"] -checksum = "sha256:3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4" -url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-arm64-musl.tar.gz" +checksum = "sha256:8254125825a3e5e834bd8a260dab4c8303f720464a741506ec27989b9ba1769b" +url = "https://unofficial-builds.nodejs.org/download/release/v24.22.1/node-v24.22.1-linux-arm64-musl.tar.gz" [tools.node."platforms.linux-x64"] -checksum = "sha256:6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff" -url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.gz" +checksum = "sha256:eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c" +url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.gz" [tools.node."platforms.linux-x64-baseline"] -checksum = "sha256:6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff" -url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.gz" +checksum = "sha256:eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c" +url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.gz" [tools.node."platforms.linux-x64-musl"] -checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a" -url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz" +checksum = "sha256:0a9eddce2d2e8f3addbe94fa46d751325b2d74f7fd1ce63296164d36b6dfc6e5" +url = "https://unofficial-builds.nodejs.org/download/release/v24.22.1/node-v24.22.1-linux-x64-musl.tar.gz" [tools.node."platforms.linux-x64-musl-baseline"] -checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a" -url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz" +checksum = "sha256:0a9eddce2d2e8f3addbe94fa46d751325b2d74f7fd1ce63296164d36b6dfc6e5" +url = "https://unofficial-builds.nodejs.org/download/release/v24.22.1/node-v24.22.1-linux-x64-musl.tar.gz" [tools.node."platforms.macos-arm64"] -checksum = "sha256:bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057" -url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz" +checksum = "sha256:89c682593d76cd44233fa53b1563264e7de9cbb189cad169135c5b4fadfbc0a7" +url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-arm64.tar.gz" [tools.node."platforms.macos-x64"] -checksum = "sha256:1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097" -url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz" +checksum = "sha256:d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035" +url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-x64.tar.gz" [tools.node."platforms.macos-x64-baseline"] -checksum = "sha256:1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097" -url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz" +checksum = "sha256:d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035" +url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-x64.tar.gz" [tools.node."platforms.windows-x64"] -checksum = "sha256:158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541" -url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip" +checksum = "sha256:d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95fa5" +url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip" [tools.node."platforms.windows-x64-baseline"] -checksum = "sha256:158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541" -url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip" +checksum = "sha256:d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95fa5" +url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip" [[tools.pnpm]] -version = "12.8.1" +version = "12.15.0" backend = "aqua:pnpm/pnpm" -specifiers = ["12.8.1"] +specifiers = ["12.15.0"] [tools.pnpm."platforms.linux-arm64"] -checksum = "sha256:75c5e34d3164fe3d3549580b1b1b59c9dd16543e558dec722b686193cdb7dfa1" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-linux-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935899" +checksum = "sha256:d7ce93a2c7d847d3a34bd3f559b60c14f578b1320c17cc352a2c5c5fdf956117" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173352" provenance = "github-attestations" [tools.pnpm."platforms.linux-arm64-musl"] -checksum = "sha256:27ad77bdf5368c1747f46fff0d0ee213ed6185aaf69e13342964f5f7cc3f8714" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-linux-arm64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935889" +checksum = "sha256:1188ef8a57bfc5786f3f739a98067242e531a4636e3f52e9e9ddc75564ea804d" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-arm64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173342" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64"] -checksum = "sha256:db3906881f63e41b655e3b97d473603dc26326c2470d0b1b8083689e1bcbd1e8" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-linux-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935887" +checksum = "sha256:55b5ed0092bd11176c1669598e231ae52e7cedac59de6efd0d415730f37d0598" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-x64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173360" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64-baseline"] -checksum = "sha256:db3906881f63e41b655e3b97d473603dc26326c2470d0b1b8083689e1bcbd1e8" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-linux-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935887" +checksum = "sha256:55b5ed0092bd11176c1669598e231ae52e7cedac59de6efd0d415730f37d0598" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-x64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173360" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64-musl"] -checksum = "sha256:f0a2db13d0a1b63c0053a5ddcfbf5c454b040e7ef54ca8771750e2cb55b86693" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-linux-x64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935901" +checksum = "sha256:d33802cc987dcfbe02a991473764ba25ad1b2344189e2097a2570baed66f008a" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-x64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173349" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64-musl-baseline"] -checksum = "sha256:f0a2db13d0a1b63c0053a5ddcfbf5c454b040e7ef54ca8771750e2cb55b86693" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-linux-x64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935901" +checksum = "sha256:d33802cc987dcfbe02a991473764ba25ad1b2344189e2097a2570baed66f008a" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-x64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173349" provenance = "github-attestations" [tools.pnpm."platforms.macos-arm64"] -checksum = "sha256:8eeeae4cd714b2f1755750d303f5b1bcfe23d95ed7245536305d0c3877c5ce41" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-darwin-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935883" +checksum = "sha256:6a41a2877773e97f19832067187cc6fc82e186d8a2cf91fec4b4c1a5fbd6ef27" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173343" provenance = "github-attestations" [tools.pnpm."platforms.macos-x64"] -checksum = "sha256:8cbc5a840b4bcd8c0da878e6616a832d88e98a8acbedf1736310b395467f4a13" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-darwin-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935890" +checksum = "sha256:2d7b336884574aee8a765bee48591212e9caf4fcf985fb3279588c1fb73fb47f" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-x64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173347" provenance = "github-attestations" [tools.pnpm."platforms.macos-x64-baseline"] -checksum = "sha256:8cbc5a840b4bcd8c0da878e6616a832d88e98a8acbedf1736310b395467f4a13" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-darwin-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935890" +checksum = "sha256:2d7b336884574aee8a765bee48591212e9caf4fcf985fb3279588c1fb73fb47f" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-x64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173347" provenance = "github-attestations" [tools.pnpm."platforms.windows-x64"] -checksum = "sha256:823fc131326afeee292e113e2299ca9851c3ee5148a7de921c897f0170de6f05" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-win32-x64.zip" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935888" +checksum = "sha256:84cf69db1be19e95bb4e5bef329291a046f2821b390d87e00f5690809d8a2ed7" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-win32-x64.zip" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173351" provenance = "github-attestations" [tools.pnpm."platforms.windows-x64-baseline"] -checksum = "sha256:823fc131326afeee292e113e2299ca9851c3ee5148a7de921c897f0170de6f05" -url = "https://github.com/pnpm/pnpm/releases/download/v12.8.1/pnpm-win32-x64.zip" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/595935888" +checksum = "sha256:84cf69db1be19e95bb4e5bef329291a046f2821b390d87e00f5690809d8a2ed7" +url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-win32-x64.zip" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173351" provenance = "github-attestations" [[tools.shellcheck]] diff --git a/mise.toml b/mise.toml index 2d2de7c3d1..5596a9bb89 100644 --- a/mise.toml +++ b/mise.toml @@ -1,4 +1,4 @@ -min_version = '2026.9.0' +min_version = '2026.10.17' [tools] go = "1.26.5" diff --git a/package.json b/package.json index 80d08e4740..ca8e4bafd7 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "devEngines": { "packageManager": { "name": "pnpm", - "version": "12.8.1", + "version": "12.15.0", "onFail": "warn" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1109b76646..d8673924f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.8.1 - version: 12.8.1 + specifier: 12.15.0 + version: 12.15.0 packages: - '@pnpm/exe.android-arm64@12.8.1': - resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} + '@pnpm/exe.android-arm64@12.15.0': + resolution: {integrity: sha512-eVCqmscY70LGvgRzu3mi8IBWBFGix+a6mNZoWmXxESsBdeiKEF9xQSuaeel10yn+zI7TXy2jlI63vFLzETk+aQ==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.8.1': - resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} + '@pnpm/exe.android-x64@12.15.0': + resolution: {integrity: sha512-Gx/WbkFcdUFUg8g8AShezcS9EiQxfYvdAm2hfKLO8sfJmJvKOCt5fzlgHWDzmGhoclibgW+tUGZ/KF4F4wOTCA==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.8.1': - resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} + '@pnpm/exe.darwin-arm64@12.15.0': + resolution: {integrity: sha512-N0ZykiDWCvqSBJvt71j7cSoBR993FqvYZFtmpCtqvW3pguiI+NAgraLPA5OTwu5mV+/d8hYpVcPktJA3xFDLtA==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.8.1': - resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} + '@pnpm/exe.darwin-x64@12.15.0': + resolution: {integrity: sha512-7Ntr2HlBUxzwWOg7NwDxCbRW/3vXuqKO+hcXm+w3SBi9rrHY53li1wLqWVT7Gjxd+lVEHM1QJZo6WJEGFU8C6A==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.8.1': - resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} + '@pnpm/exe.freebsd-x64@12.15.0': + resolution: {integrity: sha512-btOiyL7fL7W4uQGH0aqFai2TNC3IoKcZ7gNHBVbEFZt2dWrnrD0SufL3iGs3fLlmb4OtuU35uf4Zpr9V9Jh9mA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.8.1': - resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} + '@pnpm/exe.linux-arm64-musl@12.15.0': + resolution: {integrity: sha512-pNC38k+O+90FOjbPNSnw0E7apq1PKTmz3fFe1amn+lys/U5kL++uzir8bRiho8ONcyA3d9EU7wdqcY0DKkj+JA==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.8.1': - resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} + '@pnpm/exe.linux-arm64@12.15.0': + resolution: {integrity: sha512-qML/8aCXslh8td/w4U+HJNqUcZ2+m6EfTbWScIzZRmyPsRFN+xB4LP1U/hymxc1iSJiKtG56HJx2fvUErWFIGw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.8.1': - resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} + '@pnpm/exe.linux-ppc64@12.15.0': + resolution: {integrity: sha512-tjRDUUl7RCJ6T/6eoNPxOqkwU4Mzi5RtTXmSAxujyz37OTwwsMU7Ps087Zk4gHcUvRSeyF40ZFsew3vtE+nmUA==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.8.1': - resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} + '@pnpm/exe.linux-riscv64@12.15.0': + resolution: {integrity: sha512-bF1uBWEfbwaeryz6lIiaJ8AWD082otZfvzGX5l+6LZRpRwUEpJHUsJrORaYP5/cNMv5ZR2p6TvvcJcQzwtDzmw==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.8.1': - resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} + '@pnpm/exe.linux-s390x@12.15.0': + resolution: {integrity: sha512-zKMhIzsexYMToeLmqV5jPsvV/f9vwsJb2kUH2NFYzalbxImeBrlA4kL7GwF8gD4pagV6jYYcJb8RjbZIdjHptg==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.8.1': - resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} + '@pnpm/exe.linux-x64-musl@12.15.0': + resolution: {integrity: sha512-A9J0Jg0Rdx+qQU4qXtwyST5tto9IOu1zi4fDk35nbc6DWahggbcKtFvZPKFwL2qgUXGXOX//hXv6z/sp6emO8A==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.8.1': - resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} + '@pnpm/exe.linux-x64@12.15.0': + resolution: {integrity: sha512-yc+QlZR+9+nr29J4fRzhMoapgxTBDndq/+peyoq1XUufnQFnfbXUMFudDIKzHi545linGqmOmXfn0NoufNgcOg==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.8.1': - resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} + '@pnpm/exe.win32-arm64@12.15.0': + resolution: {integrity: sha512-fPXZxuY1VclkOga9bc53qNCXcegfeoBR8+Ue+AospDwKzuGGSHll0oDxkLfkmpjyRqpe7bH5DyQPYp/xSMaIVA==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.8.1': - resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} + '@pnpm/exe.win32-x64@12.15.0': + resolution: {integrity: sha512-nrd+Tp1We1DGj4DGG5Bea9lZNh9y1PjlLBdEwdInS4D0h0UIDtM7JaGGGDJhNzIwcHASyihWI7HLV+dtG+fhYg==} cpu: [x64] os: [win32] - pnpm@12.8.1: - resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} - engines: {node: '>=18.*'} + pnpm@12.15.0: + resolution: {integrity: sha512-s5UdbW/0qfE9x89WzSjSuOs328ycaOUuHhGRhz6p+CoBcoerULqtxczElPA2xnwOR+sTYIwvo29HcRth6hKOYQ==} + engines: {node: '>=18'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.8.1': + '@pnpm/exe.android-arm64@12.15.0': optional: true - '@pnpm/exe.android-x64@12.8.1': + '@pnpm/exe.android-x64@12.15.0': optional: true - '@pnpm/exe.darwin-arm64@12.8.1': + '@pnpm/exe.darwin-arm64@12.15.0': optional: true - '@pnpm/exe.darwin-x64@12.8.1': + '@pnpm/exe.darwin-x64@12.15.0': optional: true - '@pnpm/exe.freebsd-x64@12.8.1': + '@pnpm/exe.freebsd-x64@12.15.0': optional: true - '@pnpm/exe.linux-arm64-musl@12.8.1': + '@pnpm/exe.linux-arm64-musl@12.15.0': optional: true - '@pnpm/exe.linux-arm64@12.8.1': + '@pnpm/exe.linux-arm64@12.15.0': optional: true - '@pnpm/exe.linux-ppc64@12.8.1': + '@pnpm/exe.linux-ppc64@12.15.0': optional: true - '@pnpm/exe.linux-riscv64@12.8.1': + '@pnpm/exe.linux-riscv64@12.15.0': optional: true - '@pnpm/exe.linux-s390x@12.8.1': + '@pnpm/exe.linux-s390x@12.15.0': optional: true - '@pnpm/exe.linux-x64-musl@12.8.1': + '@pnpm/exe.linux-x64-musl@12.15.0': optional: true - '@pnpm/exe.linux-x64@12.8.1': + '@pnpm/exe.linux-x64@12.15.0': optional: true - '@pnpm/exe.win32-arm64@12.8.1': + '@pnpm/exe.win32-arm64@12.15.0': optional: true - '@pnpm/exe.win32-x64@12.8.1': + '@pnpm/exe.win32-x64@12.15.0': optional: true - pnpm@12.8.1: + pnpm@12.15.0: optionalDependencies: - '@pnpm/exe.android-arm64': 12.8.1 - '@pnpm/exe.android-x64': 12.8.1 - '@pnpm/exe.darwin-arm64': 12.8.1 - '@pnpm/exe.darwin-x64': 12.8.1 - '@pnpm/exe.freebsd-x64': 12.8.1 - '@pnpm/exe.linux-arm64': 12.8.1 - '@pnpm/exe.linux-arm64-musl': 12.8.1 - '@pnpm/exe.linux-ppc64': 12.8.1 - '@pnpm/exe.linux-riscv64': 12.8.1 - '@pnpm/exe.linux-s390x': 12.8.1 - '@pnpm/exe.linux-x64': 12.8.1 - '@pnpm/exe.linux-x64-musl': 12.8.1 - '@pnpm/exe.win32-arm64': 12.8.1 - '@pnpm/exe.win32-x64': 12.8.1 + '@pnpm/exe.android-arm64': 12.15.0 + '@pnpm/exe.android-x64': 12.15.0 + '@pnpm/exe.darwin-arm64': 12.15.0 + '@pnpm/exe.darwin-x64': 12.15.0 + '@pnpm/exe.freebsd-x64': 12.15.0 + '@pnpm/exe.linux-arm64': 12.15.0 + '@pnpm/exe.linux-arm64-musl': 12.15.0 + '@pnpm/exe.linux-ppc64': 12.15.0 + '@pnpm/exe.linux-riscv64': 12.15.0 + '@pnpm/exe.linux-s390x': 12.15.0 + '@pnpm/exe.linux-x64': 12.15.0 + '@pnpm/exe.linux-x64-musl': 12.15.0 + '@pnpm/exe.win32-arm64': 12.15.0 + '@pnpm/exe.win32-x64': 12.15.0 --- lockfileVersion: '9.0' From e125d0f085df8f36a1db0a5afc263227fe55c934 Mon Sep 17 00:00:00 2001 From: kanad Date: Fri, 23 Oct 2026 14:40:30 +0000 Subject: [PATCH 04/33] chore(ci): bump jactionlint to 2.3.0 and address its new rules (#7287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #7285. ## Bump - `jactionlint` moves from 1.8.2 to 2.3.0 (`"2"` in mise.toml, pinned in `mise.lock`). - The config's deprecated `require-commit-hash` key becomes `rules: unpinned-uses: error`, the vendored `.repos/**` submodules are excluded, and the `missing-timeout` and `concurrency-limits` policy rules are turned off. ## Fixes, one commit per rule - **excessive-permissions**: single-job workflows grant their write scopes on the job instead of the workflow. contribution-gate grants both jobs the scopes its script needs. release-smoke-test's caller job keeps the four scopes release-shared's job tree declares. - **github-app**: the three release-notes App tokens request only the permissions their jobs use, confirmed against GitHub's REST permission tables. - **superfluous-actions**: both `softprops/action-gh-release` steps become `gh release create`, skipping when the release already exists and verifying the tag the earlier step pushed. - **pipeline-without-pipefail**, **github-env**, **artipacked**, **unused-job-output**: small mechanical fixes. ## Accepted findings, one commit per rule Each carries an inline `# jactionlint ignore=` comment with a plain-English reason: `dangerous-triggers`, `adhoc-packages`, `unverified-download`, `cache-poisoning` (run-scoped key), `unpinned-images` (smoke-test container), `bot-conditions` (the author check is already present), and `issues: write` on reopen-stale-issue. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude --- .github/jactionlint.yaml | 10 ++- .github/scripts/ai-review/resolve.ts | 3 +- .github/workflows/ai-review.yml | 12 +++- .github/workflows/apply-release-notes.yml | 10 ++- .github/workflows/bot-pr-auto-merge.yml | 6 ++ .github/workflows/cli-go-ci.yml | 1 + .github/workflows/cli-go-mirror-image.yml | 9 +-- .github/workflows/cli-go-pg-prove.yml | 2 - .github/workflows/cli-go-publish-migra.yml | 2 - .github/workflows/cli-go-tag-pkg.yml | 5 +- .../workflows/close-stale-issues-and-prs.yml | 9 +-- .github/workflows/contribution-gate.yml | 20 ++++-- .github/workflows/dispatch-cli-e2e-ci.yml | 1 + .github/workflows/fill-sticky-disks.yml | 2 + .github/workflows/live-e2e-notify.yml | 2 + .github/workflows/propose-release-notes.yml | 3 + .github/workflows/release-config.yml | 29 ++++---- .github/workflows/release-shared.yml | 24 +++++-- .github/workflows/release-smoke-test.yml | 15 ++-- .github/workflows/reopen-stale-issue.yml | 5 ++ .github/workflows/setup-cli-smoke-test.yml | 3 + .github/workflows/sync-branches.yml | 13 +++- .github/workflows/sync-repair.yml | 9 ++- .github/workflows/verify-install-channels.yml | 3 + mise.lock | 70 +++++++++---------- mise.toml | 2 +- 26 files changed, 180 insertions(+), 90 deletions(-) diff --git a/.github/jactionlint.yaml b/.github/jactionlint.yaml index 11d6ccd219..11746e71e1 100644 --- a/.github/jactionlint.yaml +++ b/.github/jactionlint.yaml @@ -1,4 +1,7 @@ -require-commit-hash: true # actions, reusable workflows and docker:// must be pinned to a full SHA / @sha256 digest +rules: + missing-timeout: off + concurrency-limits: off + unpinned-uses: error # actions, reusable workflows and docker:// must be pinned to a full SHA / @sha256 digest self-hosted-runner: labels: - arm-runner @@ -8,3 +11,8 @@ self-hosted-runner: - blacksmith-8vcpu-ubuntu-2404 - blacksmith-8vcpu-windows-2025 - blacksmith-32vcpu-ubuntu-2404 +paths: + # Other projects' repositories, pulled in as git submodules. CI never checks them out. + ".repos/**": + ignore: + - ".*" diff --git a/.github/scripts/ai-review/resolve.ts b/.github/scripts/ai-review/resolve.ts index 0cfaafc071..74984e22ed 100644 --- a/.github/scripts/ai-review/resolve.ts +++ b/.github/scripts/ai-review/resolve.ts @@ -12,7 +12,7 @@ * * `resolveDecision` is the pure orchestration function (I/O injected) a test * can drive without the network; `main()` wires up the real GitHub I/O, - * writes `should_run`/`pr_number`/`head_ref`/`trigger` to `$GITHUB_OUTPUT`, + * writes `should_run`/`pr_number`/`head_sha`/`trigger` to `$GITHUB_OUTPUT`, * and surfaces the skip reason (if any) in `$GITHUB_STEP_SUMMARY`. * * Run in CI as: `bun .github/scripts/ai-review/resolve.ts`. @@ -374,7 +374,6 @@ function writeOutputs(result: ResolveResult, prNumber: number): void { const entries: Record = { should_run: String(result.shouldRun), pr_number: String(prNumber), - head_ref: `refs/pull/${prNumber}/head`, head_sha: result.headSha ?? "", trigger: result.trigger, }; diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 582874175f..56bbfb6926 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -16,6 +16,10 @@ on: description: "PR number to review" required: true type: string + # A `/ai-review` comment on a pull request starts a review. The resolve job checks who + # commented, and the one job that can write to the pull request runs code from the default + # branch only, never from the pull request. + # jactionlint ignore=dangerous-triggers issue_comment: types: - created @@ -65,7 +69,6 @@ jobs: outputs: should_run: ${{ steps.resolve.outputs.should_run }} pr_number: ${{ steps.resolve.outputs.pr_number }} - head_ref: ${{ steps.resolve.outputs.head_ref }} # Every job reviews this commit, so a push during the run is never reported as reviewed. head_sha: ${{ steps.resolve.outputs.head_sha }} trigger: ${{ steps.resolve.outputs.trigger }} @@ -161,7 +164,10 @@ jobs: # behavior mid-rollout. Installed from the trusted checkout with npm config isolation so a # PR-supplied `.npmrc` in the untrusted `pr` checkout can never redirect this install to a # hostile registry. - # 2.1.280 is the first release that accepts claude-opus-5-5. + # 2.1.280 is the first release that accepts claude-opus-5-5. The CLI is installed on its + # own, not through the repository's lockfile, so nothing in the pull request's checkout + # can change what gets installed. + # jactionlint ignore=adhoc-packages - name: Install Claude Code CLI working-directory: trusted run: | @@ -544,7 +550,7 @@ jobs: steps: # Security-critical: this is the only job with write permission, so it must only ever # execute trusted base-branch code — never the PR head. Checking out `develop` explicitly - # (never `needs.resolve.outputs.head_ref`) keeps a malicious PR from smuggling a script + # (never the PR's own ref) keeps a malicious PR from smuggling a script # change into the one job that can write back to the PR. (For `pull_request` events GitHub # runs the workflow file from the PR's own ref; acceptable since the auto path only admits # same-repo PRs, whose authors hold write access anyway, and fork PRs run with a read-only diff --git a/.github/workflows/apply-release-notes.yml b/.github/workflows/apply-release-notes.yml index 753028e5cc..831ec66cf7 100644 --- a/.github/workflows/apply-release-notes.yml +++ b/.github/workflows/apply-release-notes.yml @@ -27,13 +27,14 @@ jobs: outputs: authorized: ${{ steps.check.outputs.authorized }} steps: - # App token needs org membership read, repo write to edit the release, and PR write to - # comment and close. + # The token reads the approver's team membership and comments on the pull request. - id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.GH_APP_CLIENT_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + permission-members: read + permission-pull-requests: write - name: Authorize approver against supabase/cli team id: check @@ -66,11 +67,16 @@ jobs: contents: write pull-requests: write steps: + # The token edits the GitHub release, deletes the branch, and comments on and closes the + # pull request. The script below runs from the pull request's own checkout, so the token + # must not be able to change workflow files. - id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.GH_APP_CLIENT_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write # Checkout the PR head so reviewer edits made before approval are captured; # apply-release-notes.ts reads from the working tree. diff --git a/.github/workflows/bot-pr-auto-merge.yml b/.github/workflows/bot-pr-auto-merge.yml index 6745993567..3d1006bfda 100644 --- a/.github/workflows/bot-pr-auto-merge.yml +++ b/.github/workflows/bot-pr-auto-merge.yml @@ -1,6 +1,9 @@ name: Bot PR auto-merge on: + # Only this event lets the job use the auto-approve environment's secret. The job never + # checks out or runs code from the pull request. + # jactionlint ignore=dangerous-triggers pull_request_target: types: [opened, reopened, synchronize, ready_for_review] branches: [develop] @@ -14,6 +17,9 @@ jobs: # Other apps (coding agents, CI vendors) also open PRs here and still need a maintainer. # Release-notes PRs are published by a maintainer approval and must never merge. Branch sync # PRs land by fast-forward on a maintainer approval, never by a squash. + # The first condition checks who opened the pull request. The second checks who caused this + # event, so a person pushing to a bot's pull request does not get it approved. + # jactionlint ignore=bot-conditions if: >- contains(fromJSON('["dependabot[bot]", "supabase-cli-releaser[bot]"]'), github.event.pull_request.user.login) && contains(fromJSON('["dependabot[bot]", "supabase-cli-releaser[bot]"]'), github.event.sender.login) && diff --git a/.github/workflows/cli-go-ci.yml b/.github/workflows/cli-go-ci.yml index 2ae77eac79..97ee9dc277 100644 --- a/.github/workflows/cli-go-ci.yml +++ b/.github/workflows/cli-go-ci.yml @@ -31,6 +31,7 @@ jobs: # Required by: internal/utils/credentials/keyring_test.go - uses: t1m0thyj/unlock-keyring@cbcf205c879ebd86add70bab3a6abfcce59a5cae # v1.2.0 - run: | + set -o pipefail pkgs=$(go list ./pkg/... | grep -Ev 'pkg/api' | paste -sd ',' -) go tool gotestsum -- -race -v -count=1 ./... \ -coverpkg="./cmd/...,./internal/...,${pkgs}" -coverprofile=coverage.out diff --git a/.github/workflows/cli-go-mirror-image.yml b/.github/workflows/cli-go-mirror-image.yml index 889ecaf9ae..284118c964 100644 --- a/.github/workflows/cli-go-mirror-image.yml +++ b/.github/workflows/cli-go-mirror-image.yml @@ -19,14 +19,15 @@ on: required: true type: string -permissions: - contents: read - packages: write - id-token: write +permissions: {} jobs: mirror: runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write steps: - id: strip env: diff --git a/.github/workflows/cli-go-pg-prove.yml b/.github/workflows/cli-go-pg-prove.yml index 6a4414ee3f..3c836bbdce 100644 --- a/.github/workflows/cli-go-pg-prove.yml +++ b/.github/workflows/cli-go-pg-prove.yml @@ -38,8 +38,6 @@ jobs: arch: arm64 runs-on: ${{ matrix.runner }} timeout-minutes: 180 - outputs: - image_digest: ${{ steps.build.outputs.digest }} steps: - run: docker context create builders - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 diff --git a/.github/workflows/cli-go-publish-migra.yml b/.github/workflows/cli-go-publish-migra.yml index c3b0a56333..7e37210911 100644 --- a/.github/workflows/cli-go-publish-migra.yml +++ b/.github/workflows/cli-go-publish-migra.yml @@ -38,8 +38,6 @@ jobs: arch: arm64 runs-on: ${{ matrix.runner }} timeout-minutes: 180 - outputs: - image_digest: ${{ steps.build.outputs.digest }} steps: - run: docker context create builders - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 diff --git a/.github/workflows/cli-go-tag-pkg.yml b/.github/workflows/cli-go-tag-pkg.yml index 9185ad5771..3563c4077b 100644 --- a/.github/workflows/cli-go-tag-pkg.yml +++ b/.github/workflows/cli-go-tag-pkg.yml @@ -8,13 +8,14 @@ on: required: true type: string -permissions: - contents: write +permissions: {} jobs: tag: name: Create pkg tag runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/close-stale-issues-and-prs.yml b/.github/workflows/close-stale-issues-and-prs.yml index e6ecb519a3..fb43466983 100644 --- a/.github/workflows/close-stale-issues-and-prs.yml +++ b/.github/workflows/close-stale-issues-and-prs.yml @@ -22,10 +22,7 @@ on: type: string default: "security,pinned,do-not-close,keep-open,do not merge" -permissions: - contents: read - issues: write - pull-requests: write +permissions: {} concurrency: group: close-stale-issues-and-prs @@ -35,6 +32,10 @@ jobs: close-stale: runs-on: ubuntu-latest timeout-minutes: 30 + permissions: + contents: read + issues: write + pull-requests: write steps: - name: Close stale issues and PRs uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/contribution-gate.yml b/.github/workflows/contribution-gate.yml index af7cab6ac5..341bea8b29 100644 --- a/.github/workflows/contribution-gate.yml +++ b/.github/workflows/contribution-gate.yml @@ -1,6 +1,9 @@ name: Contribution Gate on: + # The gate has to comment on pull requests from forks, which only this event's token can do. + # It checks out the base branch, never the pull request. + # jactionlint ignore=dangerous-triggers pull_request_target: types: - opened @@ -16,10 +19,7 @@ on: type: boolean default: false -permissions: - pull-requests: write - issues: read - contents: read +permissions: {} concurrency: # Per-PR for pull_request_target; unique per run for manual sweeps (which @@ -31,6 +31,10 @@ jobs: gate-single: name: Contribution Gate runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: read + contents: read # Exempt maintainers/collaborators and bots. External contributors are gated. if: > github.event_name == 'pull_request_target' && @@ -42,6 +46,8 @@ jobs: # Checks out the base repo (default for pull_request_target), so the gate # runs trusted code from the base branch, never the untrusted PR head. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: ".bun-version" @@ -58,11 +64,17 @@ jobs: gate-all: name: Contribution Gate (all open PRs) runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: read + contents: read if: github.event_name == 'workflow_dispatch' steps: # Base-branch checkout only — the sweep makes API calls and never runs # any PR's code. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: ".bun-version" diff --git a/.github/workflows/dispatch-cli-e2e-ci.yml b/.github/workflows/dispatch-cli-e2e-ci.yml index ed5aa0f22a..0fc4f0a9f9 100644 --- a/.github/workflows/dispatch-cli-e2e-ci.yml +++ b/.github/workflows/dispatch-cli-e2e-ci.yml @@ -37,6 +37,7 @@ jobs: CLI_SHA: ${{ github.event.pull_request.head.sha }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | + set -o pipefail echo "Dispatching cli-e2e-ci live run for PR #${PR_NUMBER} @ ${CLI_SHA}" # Build the nested client_payload with jq — `gh api -f` sends a flat # body and would not nest `client_payload.*` correctly. diff --git a/.github/workflows/fill-sticky-disks.yml b/.github/workflows/fill-sticky-disks.yml index 2440f388d1..0e9da129b8 100644 --- a/.github/workflows/fill-sticky-disks.yml +++ b/.github/workflows/fill-sticky-disks.yml @@ -72,6 +72,7 @@ jobs: - name: Pull stack package images run: | + set -o pipefail pnpm --silent --filter @supabase/stack prepare:test-artifacts --images | xargs --max-procs 4 --max-args 1 docker pull --quiet @@ -79,6 +80,7 @@ jobs: - name: Prune Docker before saving its image disk if: always() run: | + set -o pipefail docker ps --all --quiet | xargs --no-run-if-empty docker rm --force docker system prune --force docker volume prune --all --force diff --git a/.github/workflows/live-e2e-notify.yml b/.github/workflows/live-e2e-notify.yml index 66edf219b7..441f130f15 100644 --- a/.github/workflows/live-e2e-notify.yml +++ b/.github/workflows/live-e2e-notify.yml @@ -1,6 +1,8 @@ name: Live E2E Notifications on: + # The job only reads whether the run passed. It runs nothing the run produced. + # jactionlint ignore=dangerous-triggers workflow_run: workflows: [Live E2E, Release] types: [completed] diff --git a/.github/workflows/propose-release-notes.yml b/.github/workflows/propose-release-notes.yml index 04f9359071..3ba3ad70fc 100644 --- a/.github/workflows/propose-release-notes.yml +++ b/.github/workflows/propose-release-notes.yml @@ -61,6 +61,9 @@ jobs: with: client-id: ${{ vars.GH_APP_CLIENT_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + permission-contents: write + permission-issues: read + permission-pull-requests: write - uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1 with: diff --git a/.github/workflows/release-config.yml b/.github/workflows/release-config.yml index 2b79a91614..87e6d5e57b 100644 --- a/.github/workflows/release-config.yml +++ b/.github/workflows/release-config.yml @@ -355,19 +355,24 @@ jobs: fi echo "dist-tag ${NPM_TAG} -> ${VERSION} confirmed." + # The CLI's install scripts and setup-cli resolve releases/latest/download/..., so a + # config release must never become the repo's "latest" release. Re-runs skip this step + # when the release already exists. - name: Create GitHub Release - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 - with: - token: ${{ github.token }} - tag_name: config-v${{ needs.plan.outputs.version }} - name: "@supabase/config v${{ needs.plan.outputs.version }}" - body_path: ${{ runner.temp }}/config-release/config-release-notes.md - draft: false - prerelease: false - # The CLI's install scripts and setup-cli resolve - # releases/latest/download/..., so a config release must never - # become the repo's "latest" release. - make_latest: "false" + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + tag="config-v${VERSION}" + if gh release view "${tag}" >/dev/null 2>&1; then + echo "Release ${tag} already exists; skipping." + else + gh release create "${tag}" \ + --verify-tag \ + --title "@supabase/config v${VERSION}" \ + --notes-file "${RUNNER_TEMP}/config-release/config-release-notes.md" \ + --latest=false + fi # Pages Slack the moment a real run arms the config-release approval gate. Runs in parallel # with the publish job's `waiting` state (needs only `plan`), so the ping and the pending diff --git a/.github/workflows/release-shared.yml b/.github/workflows/release-shared.yml index 1f0e0a13c9..119ee14e84 100644 --- a/.github/workflows/release-shared.yml +++ b/.github/workflows/release-shared.yml @@ -307,6 +307,8 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + # The cache key includes this run's id, so only this run's build job can have written it. + # jactionlint ignore=cache-poisoning - name: Restore build artifacts cache uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: @@ -377,13 +379,23 @@ jobs: done # Created empty; the next step uploads the assets so each one can be retried on its own. + # Re-runs skip this step when the release already exists. - name: Create draft GitHub Release - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 - with: - tag_name: v${{ inputs.version }} - name: v${{ inputs.version }} - draft: true - prerelease: ${{ inputs.prerelease }} + env: + GH_TOKEN: ${{ github.token }} + PRERELEASE: ${{ inputs.prerelease }} + run: | + set -euo pipefail + tag="v${VERSION}" + if gh release view "${tag}" >/dev/null 2>&1; then + echo "Release ${tag} already exists; skipping." + else + args=(--draft --verify-tag --title "${tag}") + if [[ "$PRERELEASE" == true ]]; then + args+=(--prerelease) + fi + gh release create "${tag}" "${args[@]}" + fi # One asset at a time with per-asset retries; see apps/cli/scripts/upload-release-assets.ts. - name: Upload release assets diff --git a/.github/workflows/release-smoke-test.yml b/.github/workflows/release-smoke-test.yml index 27ca8a2ebc..0aa4ff6b17 100644 --- a/.github/workflows/release-smoke-test.yml +++ b/.github/workflows/release-smoke-test.yml @@ -26,17 +26,18 @@ concurrency: group: release-smoke-test-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true -permissions: - # release-shared.yml declares privileged publish jobs. They are gated by - # dry_run here, but GitHub validates nested-workflow permissions at startup. - actions: read - contents: write - id-token: write - pull-requests: write +permissions: {} jobs: smoke: name: Run release smoke tests + # release-shared.yml declares privileged publish jobs. They are gated by + # dry_run here, but GitHub validates nested-workflow permissions at startup. + permissions: + actions: read + contents: write + id-token: write + pull-requests: write # Fork heads are skipped: the shared workflow declares write permissions a # fork pull request token cannot hold, and hotfixes come from this repository. if: | diff --git a/.github/workflows/reopen-stale-issue.yml b/.github/workflows/reopen-stale-issue.yml index dd7572b617..1235ddbefe 100644 --- a/.github/workflows/reopen-stale-issue.yml +++ b/.github/workflows/reopen-stale-issue.yml @@ -1,12 +1,17 @@ name: Reopen stale issue on: + # Anyone who can comment must be able to use /reopen. The script only reads the comment + # text and never runs it. + # jactionlint ignore=dangerous-triggers issue_comment: types: - created permissions: contents: read + # The script reopens the issue, removes a label, and comments. Each needs issues: write. + # jactionlint ignore=excessive-permissions issues: write concurrency: diff --git a/.github/workflows/setup-cli-smoke-test.yml b/.github/workflows/setup-cli-smoke-test.yml index dc9aeb1ba9..1bcc37242b 100644 --- a/.github/workflows/setup-cli-smoke-test.yml +++ b/.github/workflows/setup-cli-smoke-test.yml @@ -84,6 +84,9 @@ jobs: # `node:20-alpine` ships a musl-linked Node, required for JavaScript-based actions to launch # here — the runner's mounted glibc Node won't execute inside an Alpine container. container: + # The image is pinned by tag, not by digest. It only hosts the installer smoke test, and + # nothing would update a digest here automatically. + # jactionlint ignore=unpinned-images image: node:20-alpine env: VERSION: ${{ inputs.version }} diff --git a/.github/workflows/sync-branches.yml b/.github/workflows/sync-branches.yml index 5eefcfbae3..5ab7797833 100644 --- a/.github/workflows/sync-branches.yml +++ b/.github/workflows/sync-branches.yml @@ -1,6 +1,9 @@ name: Sync branches on: + # Starts when the Release workflow finishes. Every script runs from the default-branch + # checkout, never from the merged tree. + # jactionlint ignore=dangerous-triggers workflow_run: workflows: - Release @@ -120,7 +123,8 @@ jobs: persist-credentials: false - name: Load Claude model - run: echo "CLAUDE_MODEL=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_ENV" + id: model + run: echo "model=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_OUTPUT" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -134,6 +138,7 @@ jobs: bun-version-file: trusted/.bun-version # Same pinned release and isolated npm config as the AI review workflow. + # jactionlint ignore=adhoc-packages - name: Install Claude Code CLI run: | npm install -g \ @@ -146,6 +151,7 @@ jobs: working-directory: trusted env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + CLAUDE_MODEL: ${{ steps.model.outputs.model }} WORK_DIR: ${{ github.workspace }}/work run: | git -C "$WORK_DIR" config user.name "github-actions[bot]" @@ -246,7 +252,8 @@ jobs: persist-credentials: false - name: Load Claude model - run: echo "CLAUDE_MODEL=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_ENV" + id: model + run: echo "model=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_OUTPUT" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -260,6 +267,7 @@ jobs: bun-version-file: trusted/.bun-version # Same pinned release and isolated npm config as the AI review workflow. + # jactionlint ignore=adhoc-packages - name: Install Claude Code CLI run: | npm install -g \ @@ -276,6 +284,7 @@ jobs: working-directory: trusted env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + CLAUDE_MODEL: ${{ steps.model.outputs.model }} WORK_DIR: ${{ github.workspace }}/work run: | git -C "$WORK_DIR" fetch --no-tags "$OUTPUT_DIR/sync.bundle" HEAD diff --git a/.github/workflows/sync-repair.yml b/.github/workflows/sync-repair.yml index a9002a4085..dea2a6ec30 100644 --- a/.github/workflows/sync-repair.yml +++ b/.github/workflows/sync-repair.yml @@ -3,6 +3,9 @@ name: Sync repair # Once CI and the AI review finish on a Claude-resolved sync/develop-into-next pull request, lets Claude fix what # the merge broke and answer the review. See apps/cli/docs/release-process.md#claude-resolved-develop-into-next. on: + # Starts when the sync branch's checks finish. Every script runs from the default-branch + # checkout, never from the sync branch. + # jactionlint ignore=dangerous-triggers workflow_run: workflows: - CI @@ -55,7 +58,8 @@ jobs: persist-credentials: false - name: Load Claude model - run: echo "CLAUDE_MODEL=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_ENV" + id: model + run: echo "model=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_OUTPUT" - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: @@ -77,6 +81,7 @@ jobs: persist-credentials: false # Same pinned release and isolated npm config as the AI review workflow. + # jactionlint ignore=adhoc-packages - name: Install Claude Code CLI if: steps.collect.outputs.action == 'repair' run: | @@ -91,6 +96,7 @@ jobs: working-directory: trusted env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + CLAUDE_MODEL: ${{ steps.model.outputs.model }} WORK_DIR: ${{ github.workspace }}/work HEAD_SHA: ${{ steps.collect.outputs.head }} run: | @@ -119,6 +125,7 @@ jobs: HEAD_SHA: ${{ steps.collect.outputs.head }} CHANGED: ${{ steps.repair.outputs.changed }} run: | + set -o pipefail if [ "$(gh pr view "$PR" --repo "$GITHUB_REPOSITORY" --json headRefOid -q .headRefOid)" != "$HEAD_SHA" ]; then echo "::notice::The sync branch moved since the checks settled; skipping." exit 0 diff --git a/.github/workflows/verify-install-channels.yml b/.github/workflows/verify-install-channels.yml index 26bfa0d90b..66402ba70e 100644 --- a/.github/workflows/verify-install-channels.yml +++ b/.github/workflows/verify-install-channels.yml @@ -222,6 +222,9 @@ jobs: env: VERSION: ${{ inputs.version }} steps: + # This job exists to run the published install script. That script checks the tarball it + # downloads against the release's checksums.txt before using it. + # jactionlint ignore=unverified-download - name: Install via the published install script shell: bash run: | diff --git a/mise.lock b/mise.lock index 83da59b571..4b64eed8ee 100644 --- a/mise.lock +++ b/mise.lock @@ -172,74 +172,74 @@ url_api = "https://api.github.com/repos/golangci/golangci-lint/releases/assets/4 provenance = "github-attestations" [[tools.jactionlint]] -version = "1.8.2" +version = "2.3.0" backend = "github:jdx/jactionlint" -specifiers = ["1"] +specifiers = ["2"] [tools.jactionlint."platforms.linux-arm64"] -checksum = "sha256:7d618749dbddd762a14bd491ea97bc4f6599626f830a11edc6c6522ead8bcd05" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338576" +checksum = "sha256:deea7744e5769e1e5cc8c9898a838f80a941ec68476d9f2814a61474d5652dac" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210370" provenance = "github-attestations" [tools.jactionlint."platforms.linux-arm64-musl"] -checksum = "sha256:7d618749dbddd762a14bd491ea97bc4f6599626f830a11edc6c6522ead8bcd05" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338576" +checksum = "sha256:deea7744e5769e1e5cc8c9898a838f80a941ec68476d9f2814a61474d5652dac" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210370" provenance = "github-attestations" [tools.jactionlint."platforms.linux-x64"] -checksum = "sha256:6533bd8047fc012a3f66238d84f65cacdac669da00b9f6cf042c1734c252bc93" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338580" +checksum = "sha256:907e6f6a59e181879f7e6936e19ee2eca1cbc51de134fb85460cfa8d65a71f28" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210498" provenance = "github-attestations" [tools.jactionlint."platforms.linux-x64-baseline"] -checksum = "sha256:6533bd8047fc012a3f66238d84f65cacdac669da00b9f6cf042c1734c252bc93" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338580" +checksum = "sha256:907e6f6a59e181879f7e6936e19ee2eca1cbc51de134fb85460cfa8d65a71f28" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210498" provenance = "github-attestations" [tools.jactionlint."platforms.linux-x64-musl"] -checksum = "sha256:6533bd8047fc012a3f66238d84f65cacdac669da00b9f6cf042c1734c252bc93" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338580" +checksum = "sha256:907e6f6a59e181879f7e6936e19ee2eca1cbc51de134fb85460cfa8d65a71f28" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210498" provenance = "github-attestations" [tools.jactionlint."platforms.linux-x64-musl-baseline"] -checksum = "sha256:6533bd8047fc012a3f66238d84f65cacdac669da00b9f6cf042c1734c252bc93" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338580" +checksum = "sha256:907e6f6a59e181879f7e6936e19ee2eca1cbc51de134fb85460cfa8d65a71f28" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210498" provenance = "github-attestations" [tools.jactionlint."platforms.macos-arm64"] -checksum = "sha256:de9eb05af3cde409dc0b37fe2fd6a21b875a575374e7b26c02d45a7086645c28" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_darwin_arm64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338605" +checksum = "sha256:f96afa36578b21290203ae8df4fa7ac3bf5aba08ed288df28cb0530af7ca6e55" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210452" provenance = "github-attestations" [tools.jactionlint."platforms.macos-x64"] -checksum = "sha256:42790b2565759b7f4a81696fb9034b2060f97f551d2ced54cf722c9de9abe6b2" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_darwin_amd64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338604" +checksum = "sha256:a61006b669ec0ab1266c73e3561eecf839537eb6e8b3b6be766a55ba17fcbe42" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_darwin_amd64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210428" provenance = "github-attestations" [tools.jactionlint."platforms.macos-x64-baseline"] -checksum = "sha256:42790b2565759b7f4a81696fb9034b2060f97f551d2ced54cf722c9de9abe6b2" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_darwin_amd64.tar.gz" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338604" +checksum = "sha256:a61006b669ec0ab1266c73e3561eecf839537eb6e8b3b6be766a55ba17fcbe42" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_darwin_amd64.tar.gz" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210428" provenance = "github-attestations" [tools.jactionlint."platforms.windows-x64"] -checksum = "sha256:fd7be539cd3276d5178f85ef140c1bb52395b1ec1f59b1585050e1be3b0b194b" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_windows_amd64.zip" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338616" +checksum = "sha256:9864d1045bd37fddb2df33bf4d6b45de205c0ce133c52a225928b7ede7d7f3eb" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_windows_amd64.zip" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210534" provenance = "github-attestations" [tools.jactionlint."platforms.windows-x64-baseline"] -checksum = "sha256:fd7be539cd3276d5178f85ef140c1bb52395b1ec1f59b1585050e1be3b0b194b" -url = "https://github.com/jdx/jactionlint/releases/download/v1.8.2/jactionlint_1.8.2_windows_amd64.zip" -url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/611338616" +checksum = "sha256:9864d1045bd37fddb2df33bf4d6b45de205c0ce133c52a225928b7ede7d7f3eb" +url = "https://github.com/jdx/jactionlint/releases/download/v2.3.0/jactionlint_2.3.0_windows_amd64.zip" +url_api = "https://api.github.com/repos/jdx/jactionlint/releases/assets/662210534" provenance = "github-attestations" [[tools.node]] diff --git a/mise.toml b/mise.toml index 5596a9bb89..a197d4db4c 100644 --- a/mise.toml +++ b/mise.toml @@ -3,7 +3,7 @@ min_version = '2026.10.17' [tools] go = "1.26.5" golangci-lint = "2.12.2" -jactionlint = "1" +jactionlint = "2" shellcheck = "0.11.0" [settings] From f1834ed3b297a2c307cf26cdd381b7b9a8640eb2 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:45:27 +0000 Subject: [PATCH 05/33] chore(stack): bump storage to v1.84.22-r0 (#7306) This bumps storage from v1.84.21-r0 to v1.84.22-r0. https://github.com/supabase/slim-services/releases/tag/storage-v1.84.22-r0 Planned after supabase/slim-services published v1.84.22-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 0446e47bdf..bbc79eaeee 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.147.1 AS realtime -FROM supabase/storage-api:v1.84.21 AS storage +FROM supabase/storage-api:v1.84.22 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 0446e47bdf..bbc79eaeee 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.147.1 AS realtime -FROM supabase/storage-api:v1.84.21 AS storage +FROM supabase/storage-api:v1.84.22 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index b40b1aa6d4..bfeec14327 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -343,23 +343,23 @@ const definitions: Readonly> = { storage: definition( "storage", { - upstreamVersion: "v1.84.21", + upstreamVersion: "v1.84.22", revision: 0, image: - "ghcr.io/supabase/cli/storage:v1.84.21-r0@sha256:8e77b412dd97a53b9e88beab2497bbf6899bbce3f3e593508e2a145cf3eb913a", - upstreamImage: "supabase/storage-api:v1.84.21", + "ghcr.io/supabase/cli/storage:v1.84.22-r0@sha256:606abe71f6979a315b786d74ee19fbea77147a03e85ee3a24af3eb7e585948e1", + upstreamImage: "supabase/storage-api:v1.84.22", natives: { "darwin-arm64": { - archive: "9242f9311ab336ec1f82e91b15bd62a1b1d1ee6602b5e3337f6b314b5d5487d6", - manifest: "0917e94e57a726b345636a5f606b60132c9102f9d1a1f412d475fc9ac10a546c", + archive: "299655c883ed563691f7c8043fcf37d018a0c9c66610445e0a59d7864d838066", + manifest: "83653312597a56018cb9c78b90f6e00bacb037ae64c807f9ced1d61d7d4559be", }, "linux-amd64": { - archive: "9a57bc5d5027c7a2bf7b995ac88168a590f599b2eca63047499ac7a1f2dfc232", - manifest: "45b4439c92c83377d03bf9e2bf3c28bf9d164329a8100573d52f80108a318806", + archive: "c0e9090f0cc5b160fe0da1c85046f1ac33361d297ad234e6376b8e75f6cdc3a1", + manifest: "bed3c914dc7db44b678c6cc570c4414d41ee102967973729b3f047ba732b614c", }, "linux-arm64": { - archive: "2b00c4c8ab62e3ca2508ae237ec3ead78152d5d1dbb68c0fa291977f3ed2e2ab", - manifest: "83aae8b24999bb5647673116a523ed4a5e75e103262b5fc4912e5ba4e0258671", + archive: "a567ec9a447d97d38671436b7aadbd0c438dfc1eef9221f637c251eb04be820a", + manifest: "16075fc56dad79f79e0470f8d2b39371e3cb73958f1fa0c08f728894cf6fa681", }, }, }, From 97407412e422ab34123ea48cfc453151a4aaba97 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:43:10 +0000 Subject: [PATCH 06/33] chore(stack): bump storage to v1.84.23-r0 (#7308) This bumps storage from v1.84.22-r0 to v1.84.23-r0. https://github.com/supabase/slim-services/releases/tag/storage-v1.84.23-r0 Planned after supabase/slim-services published v1.84.23-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index bbc79eaeee..d00ee9f216 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.147.1 AS realtime -FROM supabase/storage-api:v1.84.22 AS storage +FROM supabase/storage-api:v1.84.23 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index bbc79eaeee..d00ee9f216 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.147.1 AS realtime -FROM supabase/storage-api:v1.84.22 AS storage +FROM supabase/storage-api:v1.84.23 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index bfeec14327..2775bdba2a 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -343,23 +343,23 @@ const definitions: Readonly> = { storage: definition( "storage", { - upstreamVersion: "v1.84.22", + upstreamVersion: "v1.84.23", revision: 0, image: - "ghcr.io/supabase/cli/storage:v1.84.22-r0@sha256:606abe71f6979a315b786d74ee19fbea77147a03e85ee3a24af3eb7e585948e1", - upstreamImage: "supabase/storage-api:v1.84.22", + "ghcr.io/supabase/cli/storage:v1.84.23-r0@sha256:069a20dea29b554e77f49b1202401f533d3d00b3d067460a06d9a8ecc8c9d936", + upstreamImage: "supabase/storage-api:v1.84.23", natives: { "darwin-arm64": { - archive: "299655c883ed563691f7c8043fcf37d018a0c9c66610445e0a59d7864d838066", - manifest: "83653312597a56018cb9c78b90f6e00bacb037ae64c807f9ced1d61d7d4559be", + archive: "cb37256862e36a349d42e70a0c72e440d4bd115151c84fd86623b7de099f09dc", + manifest: "3195d4c7a04d62e72fa7d7c47ef7957cbba0924fdcbe6f3aec5aa6799a6deeda", }, "linux-amd64": { - archive: "c0e9090f0cc5b160fe0da1c85046f1ac33361d297ad234e6376b8e75f6cdc3a1", - manifest: "bed3c914dc7db44b678c6cc570c4414d41ee102967973729b3f047ba732b614c", + archive: "4f266da712632ca53ee9644e4174c178dde1a7bd6f466893ac784a82a27e5579", + manifest: "df83e25170e057fca96b249546f2b198f7caec64531a557d1aafd283ec2059bc", }, "linux-arm64": { - archive: "a567ec9a447d97d38671436b7aadbd0c438dfc1eef9221f637c251eb04be820a", - manifest: "16075fc56dad79f79e0470f8d2b39371e3cb73958f1fa0c08f728894cf6fa681", + archive: "747169a52dab3acda568dccb0559fb27ae249bdb08b2361c477f4ba27626a4b6", + manifest: "ddb9c6dd2637e7415c87c7db604d8adadf8e65bd800c254c04256705f5750dc9", }, }, }, From 666bb77bf5caec289e1218048aad64c5f2b3d494 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:56:46 +0000 Subject: [PATCH 07/33] chore(stack): bump realtime to v2.147.2-r0 (#7314) This bumps realtime from v2.147.1-r0 to v2.147.2-r0. https://github.com/supabase/slim-services/releases/tag/realtime-v2.147.2-r0 Planned after supabase/slim-services published v2.147.2-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index d00ee9f216..089153a7e1 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.147.1 AS realtime +FROM supabase/realtime:v2.147.2 AS realtime FROM supabase/storage-api:v1.84.23 AS storage diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index d00ee9f216..089153a7e1 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.147.1 AS realtime +FROM supabase/realtime:v2.147.2 AS realtime FROM supabase/storage-api:v1.84.23 AS storage diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 2775bdba2a..dc92143b25 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -317,23 +317,23 @@ const definitions: Readonly> = { realtime: definition( "realtime", { - upstreamVersion: "v2.147.1", + upstreamVersion: "v2.147.2", revision: 0, image: - "ghcr.io/supabase/cli/realtime:v2.147.1-r0@sha256:49a10b8b4e6e5ee4d94e31477ad97f462ef220a842e5859b76114b15e41b53e1", - upstreamImage: "supabase/realtime:v2.147.1", + "ghcr.io/supabase/cli/realtime:v2.147.2-r0@sha256:3d131d1d58ba947c5f9323c05774de84f0d685e048437260b3b1ecf6384140a6", + upstreamImage: "supabase/realtime:v2.147.2", natives: { "darwin-arm64": { - archive: "fd05e59cf29c6e8e0523f60ca0f3e823d045178fa7ebdcba934d116c0889c15a", - manifest: "f85b0ae9b9a10671f892298e68912ed0fc2e6828427e7d039d0651edd5600339", + archive: "f639b9bcf8ae009ffb143c8326c165adbaf14ad4f77bd9f8b742aa226d372541", + manifest: "fd607c4f97f86c04818686e6d378ed7e75e9bd83a4a38c813d8d9dd7d784139c", }, "linux-amd64": { - archive: "4f50539770db276c890dd582fef5239a3800a4d074311ec739227738e5f3f576", - manifest: "43651c3933e26580605390cb000fdc36603213e10b2a5fbbeaf519178e6eea9c", + archive: "a85921940349cf867eaf083d62fc576d67d670757f6178cd9c005c5e1882d4a4", + manifest: "21bf7c0ff1ede326bc60a3cd6998bde3999d1261e2e2137e51c9a2ea6290adb9", }, "linux-arm64": { - archive: "f04da0eaec820690f24a37da0d2b0787e41ef8c6f0d28935639cf6c87d2d0162", - manifest: "705d7c7321537acbb6cbb1b2b3efef5f19dc9a167c31d960082e0323ab200120", + archive: "4d79aa39d5a4dbf9e961906c06bac43fff436febf9ff19074306fc061e7cc0a1", + manifest: "e5b6a5132774db39d2e4e7b40d521800d2ea40227b2be789334bb765b04f523b", }, }, }, From 1d50d0ab76eec90d958efc0fd2b6b3ae0a61683d Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:06:12 +0000 Subject: [PATCH 08/33] chore: sync API types from infrastructure (#7316) This PR was automatically created to sync API types from the infrastructure repository. Changes were detected after refreshing `apps/cli-go/api/v1-openapi.yaml` from the latest spec from infrastructure, and `apps/cli-go/pkg/api` is regenerated from it. The snapshot diff is the upstream API change and is worth reading: it is marked `linguist-generated`, so GitHub collapses it behind a "Load diff" click rather than hiding it. Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> --- apps/cli-go/api/v1-openapi.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/cli-go/api/v1-openapi.yaml b/apps/cli-go/api/v1-openapi.yaml index f0b48a3fad..cf6bf4a56a 100644 --- a/apps/cli-go/api/v1-openapi.yaml +++ b/apps/cli-go/api/v1-openapi.yaml @@ -1614,6 +1614,8 @@ paths: description: Unauthorized '403': description: Forbidden action + '404': + description: API key not found for project '429': description: Rate limit exceeded security: @@ -1674,6 +1676,8 @@ paths: description: Unauthorized '403': description: Forbidden action + '404': + description: API key not found for project '429': description: Rate limit exceeded security: @@ -1744,6 +1748,8 @@ paths: description: Unauthorized '403': description: Forbidden action + '404': + description: API key not found for project '429': description: Rate limit exceeded security: From 466485003e9fc4a3c15bf80b5369fddba65c2846 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:38:57 +0000 Subject: [PATCH 09/33] chore(stack): bump realtime to v2.147.3-r0 (#7317) This bumps realtime from v2.147.2-r0 to v2.147.3-r0. https://github.com/supabase/slim-services/releases/tag/realtime-v2.147.3-r0 Planned after supabase/slim-services published v2.147.3-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 089153a7e1..b4ff069fc2 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.147.2 AS realtime +FROM supabase/realtime:v2.147.3 AS realtime FROM supabase/storage-api:v1.84.23 AS storage diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 089153a7e1..b4ff069fc2 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.147.2 AS realtime +FROM supabase/realtime:v2.147.3 AS realtime FROM supabase/storage-api:v1.84.23 AS storage diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index dc92143b25..0b2b8d2dd2 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -317,23 +317,23 @@ const definitions: Readonly> = { realtime: definition( "realtime", { - upstreamVersion: "v2.147.2", + upstreamVersion: "v2.147.3", revision: 0, image: - "ghcr.io/supabase/cli/realtime:v2.147.2-r0@sha256:3d131d1d58ba947c5f9323c05774de84f0d685e048437260b3b1ecf6384140a6", - upstreamImage: "supabase/realtime:v2.147.2", + "ghcr.io/supabase/cli/realtime:v2.147.3-r0@sha256:a2c714566a075fa9f39eeea88d659b8aba5fedaddd80755bc670c6c9356de5c0", + upstreamImage: "supabase/realtime:v2.147.3", natives: { "darwin-arm64": { - archive: "f639b9bcf8ae009ffb143c8326c165adbaf14ad4f77bd9f8b742aa226d372541", - manifest: "fd607c4f97f86c04818686e6d378ed7e75e9bd83a4a38c813d8d9dd7d784139c", + archive: "1cd9a606c9b73003deef6b41b308fe7401e22204f019fe9c3d6fec384eccb72d", + manifest: "37d1cbc4e48c0d934d183504369b22fa0c8101f6ffddd38b7e45f0314590cbee", }, "linux-amd64": { - archive: "a85921940349cf867eaf083d62fc576d67d670757f6178cd9c005c5e1882d4a4", - manifest: "21bf7c0ff1ede326bc60a3cd6998bde3999d1261e2e2137e51c9a2ea6290adb9", + archive: "0f48d7a74782e9ea55ea57f77599a6a87978862f573e3a1acb20fe17426b0933", + manifest: "e235af418fafcbe44c1d5e884e79cfe868b3a152a3159649b99fc1793f614397", }, "linux-arm64": { - archive: "4d79aa39d5a4dbf9e961906c06bac43fff436febf9ff19074306fc061e7cc0a1", - manifest: "e5b6a5132774db39d2e4e7b40d521800d2ea40227b2be789334bb765b04f523b", + archive: "153f356b6008de522927b4204a4b4ca9a5389870bcb8591063f8228cad79cada", + manifest: "11c29d1e86f006b0475d622f0261d4deaab8754d779183b41eea099c514c51f1", }, }, }, From 41d66c9d221e87c06111f560552cca2c0064d210 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:01:44 +0000 Subject: [PATCH 10/33] chore(stack): bump studio to 2026.10.26-sha-478bd88-r0 (#7323) This bumps studio from 2026.10.19-sha-b375769-r0 to 2026.10.26-sha-478bd88-r0. https://github.com/supabase/slim-services/releases/tag/studio-2026.10.26-sha-478bd88-r0 Planned after supabase/slim-services published 2026.10.26-sha-478bd88-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index b4ff069fc2..39121e0d70 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -21,7 +21,7 @@ FROM postgrest/postgrest:v16.5 AS postgrest FROM supabase/postgres-meta:v0.100.0 AS pgmeta -FROM supabase/studio:2026.10.19-sha-b375769 AS studio +FROM supabase/studio:2026.10.26-sha-478bd88 AS studio FROM darthsim/imgproxy:v3.26.0 AS imgproxy diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index b4ff069fc2..39121e0d70 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -21,7 +21,7 @@ FROM postgrest/postgrest:v16.5 AS postgrest FROM supabase/postgres-meta:v0.100.0 AS pgmeta -FROM supabase/studio:2026.10.19-sha-b375769 AS studio +FROM supabase/studio:2026.10.26-sha-478bd88 AS studio FROM darthsim/imgproxy:v3.26.0 AS imgproxy diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 0b2b8d2dd2..7925711fb1 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -419,23 +419,23 @@ const definitions: Readonly> = { studio: definition( "studio", { - upstreamVersion: "2026.10.19-sha-b375769", + upstreamVersion: "2026.10.26-sha-478bd88", revision: 0, image: - "ghcr.io/supabase/cli/studio:2026.10.19-sha-b375769-r0@sha256:aef5aadd8f3a449998850ee1bc40b2c0d765555756df5779dd6d7208d6d77a93", - upstreamImage: "supabase/studio:2026.10.19-sha-b375769", + "ghcr.io/supabase/cli/studio:2026.10.26-sha-478bd88-r0@sha256:5a86b793ea2a9c46bc68911881e484eab05dc1143abc9efe46604f8a90df2d3f", + upstreamImage: "supabase/studio:2026.10.26-sha-478bd88", natives: { "darwin-arm64": { - archive: "6ee00298ee5566b442b359d4accb0f12a6deb1f26b01188a49225b978a95b04c", - manifest: "be6008eef35a121a61ca94da3135436f3e638bb70fcc47f378ba66fd06333c4f", + archive: "0f9f1a9dd0a6e9d24d88ddf85aa0b813878aa4b7ac09cd126e45a6b43bf322f9", + manifest: "5e0f8aa4180b85f58a8affd5c86d6cade3d6cce14267c6d4869ec8531158c010", }, "linux-amd64": { - archive: "4a83bf5134cb4306b12d47b993def4e54e0675accec097ab3fc2dea3626aa6d0", - manifest: "938a6290af7f138ebf025bfd5cf9482c236745ac5819748ddbfdeaf3cd4c328b", + archive: "82ab932598eafab0709570885ac6d071881be943d3b42a8a94d249024aac83dd", + manifest: "66520a9d0aabb08637c7a36972f42f0e01a3f19d9ac456d5d70e01338ce7a121", }, "linux-arm64": { - archive: "9804db2a36abb418194026dee2aaa9e3ae5d52386346576f72fc64c0420795ac", - manifest: "df0719d05a2be15f015db9259c35445ea69be5ad86546f3cad79a5652d28fcd7", + archive: "b5812235d38c09a08a130181bf0338b930e4a56573fa414ba82c4dde9ed3e2d2", + manifest: "0cc6d656347087295acc23a5013c48ef6129f06225a222c2ef9986be92fa18cc", }, }, }, From e2a524f360cbad598e3feb5dab82451b48ea8057 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Mon, 26 Oct 2026 07:28:25 +0000 Subject: [PATCH 11/33] feat(cli): carry plan-gate entitlement on machine-mode errors (#7271) Plan-gated Management API denials already return `{ error: { code: "entitlement_required", feature, upgrade_url } }`. In `--output-format json` and `stream-json` that signal only survives as escaped text inside `error.message`, so an agent has to parse prose to learn that an upgrade is needed and where. I added a structured `entitlement` to the machine-mode error object for every command whose status errors go through `mapHttpError`. ```json {"_tag":"Error","error":{"code":"DomainsUnexpectedStatusError","message":"...","entitlement":{"feature":"custom_domain","upgrade_url":"https://supabase.com/dashboard/org//billing"}}} ``` **Changed:** - **Machine-mode errors carry the plan gate**: on a 4xx, `mapHttpError` decodes the untruncated body and, when it is the plan-gate envelope, attaches `entitlement: { feature, upgrade_url }` to the status error. `normalizeCliError` passes it into the json and stream-json error object, so commands with no per-command upgrade wiring (`branches unpause`) get it too. - **Handlers that rebuild a mapped error keep it**: `branches create`/`update`, `vanity-subdomains activate`/`check-availability` and `sso list`/`add`/`update`/`remove` forward the field, and `Output.fail` declares it in its type. - **Envelope values are validated in one place**: I moved the parser into `http-errors.ts` and `suggestUpgrade` reuses it. `feature` must be a plain key and `upgrade_url` an http(s) URL without credentials, emitted as the parsed URL so control and bidi characters arrive percent-encoded; anything else counts as no envelope. **Note:** text output and `cli_upgrade_suggested` are unchanged for well-formed envelopes. The per-command text hint stays until I move the hint and the event onto the same central path in a follow-up. ## Linear - part of GROWTH-1050 --- apps/cli/AGENTS.md | 4 + apps/cli/src/command-internal/http-errors.ts | 45 +++++++++ .../command-internal/http-errors.unit.test.ts | 94 +++++++++++++++++++ .../src/command-internal/upgrade-suggest.ts | 32 +------ .../src/commands/branches/branches.errors.ts | 3 + .../branches/create/create.handler.ts | 1 + .../create/create.integration.test.ts | 27 ++++++ .../unpause/unpause.integration.test.ts | 31 ++++++ .../branches/update/update.handler.ts | 1 + apps/cli/src/commands/sso/add/add.handler.ts | 1 + .../cli/src/commands/sso/list/list.handler.ts | 1 + .../src/commands/sso/remove/remove.handler.ts | 1 + apps/cli/src/commands/sso/sso.errors.ts | 5 + .../src/commands/sso/update/update.handler.ts | 2 + .../activate/activate.handler.ts | 1 + .../check-availability.handler.ts | 1 + .../vanity-subdomains.errors.ts | 3 + apps/cli/src/shared/output/normalize-error.ts | 17 ++++ apps/cli/src/shared/output/output.layer.ts | 25 ++++- apps/cli/src/shared/output/output.service.ts | 2 + 20 files changed, 265 insertions(+), 32 deletions(-) diff --git a/apps/cli/AGENTS.md b/apps/cli/AGENTS.md index 99426185fb..4322da3c07 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -161,6 +161,10 @@ text layer. Therefore stdout is payload-only for either machine flag. Do not cha layer to move spinners; that would change text output globally. Handlers check `--output` first and use established encoders before the `output.format` branch. +Plan-gated denials carry an optional `entitlement: { feature, upgrade_url }` on the json and +stream-json error object. `mapHttpError` attaches it from the Management API's +`entitlement_required` body, so a handler that rebuilds a mapped status error must forward it. + ## Tests Follow the root testing philosophy and use the package scripts in `package.json`. Cover meaningful diff --git a/apps/cli/src/command-internal/http-errors.ts b/apps/cli/src/command-internal/http-errors.ts index eb3f93a9c5..168deb48d0 100644 --- a/apps/cli/src/command-internal/http-errors.ts +++ b/apps/cli/src/command-internal/http-errors.ts @@ -3,6 +3,8 @@ import { Effect } from "effect"; import * as HttpBody from "effect/http/HttpBody"; import * as HttpClientError from "effect/http/HttpClientError"; +import type { PlanGateEntitlement } from "../shared/output/normalize-error.ts"; + // HttpClientError reasons that indicate the server returned an actual response (vs a transport // failure). Anything in this set surfaces as an `UnexpectedStatusError`; everything else maps // to a `NetworkError`. @@ -95,8 +97,44 @@ export type StatusErrorFactory = new (args: { readonly status: number; readonly body: string; readonly message: string; + readonly entitlement?: PlanGateEntitlement; }) => E; +const FEATURE_KEY_PATTERN = /^[\w.-]{1,128}$/; +const MAX_UPGRADE_URL_LEN = 2048; + +function readString(obj: unknown, key: string): string { + if (typeof obj === "object" && obj !== null && key in obj) { + const value = (obj as Record)[key]; + return typeof value === "string" ? value : ""; + } + return ""; +} + +// The parsed `href` percent-encodes control and bidi characters, so the URL is safe to print. +function normalizeUpgradeUrl(raw: string): string | undefined { + if (raw.length > MAX_UPGRADE_URL_LEN || !URL.canParse(raw)) return undefined; + const url = new URL(raw); + const isWebUrl = url.protocol === "https:" || url.protocol === "http:"; + const hasCredentials = url.username !== "" || url.password !== ""; + return isWebUrl && !hasCredentials ? url.href : undefined; +} + +/** + * Reads the Management API's plan-gate error body, + * `{ error: { code: "entitlement_required", feature, upgrade_url } }`. + * Hand-validated: the packages/api codegen emits no non-2xx schemas. + */ +export function parsePlanGateEnvelope(body: unknown): PlanGateEntitlement | undefined { + if (typeof body !== "object" || body === null) return undefined; + const error = (body as { readonly error?: unknown }).error; + if (readString(error, "code") !== "entitlement_required") return undefined; + const feature = readString(error, "feature"); + const upgradeUrl = normalizeUpgradeUrl(readString(error, "upgrade_url")); + if (!FEATURE_KEY_PATTERN.test(feature) || upgradeUrl === undefined) return undefined; + return { feature, upgrade_url: upgradeUrl }; +} + /** * Builds an error mapper that classifies a `SupabaseApiError` into either a * typed network error or a typed unexpected-status error. Shared by command @@ -128,11 +166,18 @@ export function mapHttpError(opts: { Effect.orElseSucceed(() => cause.reason.description ?? ""), ); const body = sanitizeErrorBody(rawBody); + const entitlement = + status >= 400 && status < 500 + ? parsePlanGateEnvelope( + yield* cause.response.json.pipe(Effect.orElseSucceed(() => undefined)), + ) + : undefined; return yield* Effect.fail( new opts.statusError({ status, body, message: opts.statusMessage(status, body), + ...(entitlement === undefined ? {} : { entitlement }), }), ); } diff --git a/apps/cli/src/command-internal/http-errors.unit.test.ts b/apps/cli/src/command-internal/http-errors.unit.test.ts index aa430d1532..85b8058133 100644 --- a/apps/cli/src/command-internal/http-errors.unit.test.ts +++ b/apps/cli/src/command-internal/http-errors.unit.test.ts @@ -2,6 +2,9 @@ import { describe, expect, it } from "@effect/vitest"; import { SupabaseApiInputError } from "@supabase/api/effect"; import { Data, Effect } from "effect"; import * as HttpBody from "effect/http/HttpBody"; +import * as HttpClientError from "effect/http/HttpClientError"; +import * as HttpClientRequest from "effect/http/HttpClientRequest"; +import * as HttpClientResponse from "effect/http/HttpClientResponse"; import { classifyCliErrorActionability } from "../shared/telemetry/error-actionability.ts"; import { sanitizeInlineName, mapHttpError, unexpectedStatusMessage } from "./http-errors.ts"; @@ -60,6 +63,97 @@ describe("mapHttpError", () => { ); }); +const UPGRADE_URL = "https://supabase.com/dashboard/org/acme/billing"; + +function statusFailure(status: number, body: string): HttpClientError.HttpClientError { + const request = HttpClientRequest.get("https://api.supabase.com/v1/projects/test/backups"); + const response = HttpClientResponse.fromWeb(request, new Response(body, { status })); + return new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ request, response }), + }); +} + +function planGateBody(error: Record, message = "Upgrade required"): string { + return JSON.stringify({ message, error: { code: "entitlement_required", ...error } }); +} + +describe("mapHttpError plan-gate entitlement", () => { + it.effect("attaches the entitlement from a gated body longer than the embedded body cap", () => + Effect.gen(function* () { + const body = planGateBody({ feature: "backups", upgrade_url: UPGRADE_URL }, "x".repeat(2000)); + + const error = yield* mapError(statusFailure(402, body)).pipe(Effect.flip); + + expect(error).toMatchObject({ + _tag: "TestStatusError", + status: 402, + entitlement: { feature: "backups", upgrade_url: UPGRADE_URL }, + }); + }), + ); + + it.effect.each(["", "Bad Gateway", '{"message":"Not Found"}'])( + "keeps the plain status error for a non-envelope 4xx body %j", + (body) => + Effect.gen(function* () { + const error = yield* mapError(statusFailure(403, body)).pipe(Effect.flip); + + expect(error).toMatchObject({ _tag: "TestStatusError", status: 403, body }); + expect(error).not.toHaveProperty("entitlement"); + }), + ); + + it.effect("ignores a plan-gate body on a 5xx", () => + Effect.gen(function* () { + const body = planGateBody({ feature: "backups", upgrade_url: UPGRADE_URL }); + + const error = yield* mapError(statusFailure(503, body)).pipe(Effect.flip); + + expect(error).not.toHaveProperty("entitlement"); + }), + ); + + it.effect.each([ + { case: "a non-web upgrade_url", feature: "backups", upgradeUrl: "javascript:alert(1)" }, + { + case: "an upgrade_url carrying credentials", + feature: "backups", + upgradeUrl: "https://supabase.com@evil.example/billing", + }, + { + case: "a feature carrying a bidi override", + feature: `backups${String.fromCodePoint(0x202e)}`, + upgradeUrl: UPGRADE_URL, + }, + ])("drops the entitlement for $case", ({ feature, upgradeUrl }) => + Effect.gen(function* () { + const body = planGateBody({ feature, upgrade_url: upgradeUrl }); + + const error = yield* mapError(statusFailure(402, body)).pipe(Effect.flip); + + expect(error).toMatchObject({ _tag: "TestStatusError", status: 402 }); + expect(error).not.toHaveProperty("entitlement"); + }), + ); + + it.effect("percent-encodes control and bidi characters in the upgrade_url", () => + Effect.gen(function* () { + const rlo = String.fromCodePoint(0x202e); + const csi = String.fromCodePoint(0x9b); + const body = planGateBody({ + feature: "backups", + upgrade_url: `${UPGRADE_URL}?ref=${rlo}${csi}`, + }); + + const error = yield* mapError(statusFailure(402, body)).pipe(Effect.flip); + + expect(error).toMatchObject({ + entitlement: { feature: "backups", upgrade_url: `${UPGRADE_URL}?ref=%E2%80%AE%C2%9B` }, + }); + }), + ); +}); + describe("unexpectedStatusMessage", () => { it("shapes the generic unexpected-status message", () => { expect(unexpectedStatusMessage(500, '{"message":"boom"}')).toBe( diff --git a/apps/cli/src/command-internal/upgrade-suggest.ts b/apps/cli/src/command-internal/upgrade-suggest.ts index e45dfec831..23202f3593 100644 --- a/apps/cli/src/command-internal/upgrade-suggest.ts +++ b/apps/cli/src/command-internal/upgrade-suggest.ts @@ -14,6 +14,7 @@ import { PropOrgSlug, } from "../shared/telemetry/event-catalog.ts"; import { Output } from "../shared/output/output.service.ts"; +import { parsePlanGateEnvelope } from "./http-errors.ts"; import { billingUrl } from "./profile.ts"; import { block } from "./command-permissions/model.ts"; @@ -33,33 +34,6 @@ export const upgradeSuggestion = block("upgrade-suggest", [ }, ]); -function readString(obj: unknown, key: string): string { - if (typeof obj === "object" && obj !== null && key in obj) { - const value = (obj as Record)[key]; - return typeof value === "string" ? value : ""; - } - return ""; -} - -interface PlanGateEnvelope { - readonly feature: string; - readonly upgradeUrl: string; -} - -// Hand-validated: the packages/api codegen emits no non-2xx schemas. -function parsePlanGateEnvelope(body: unknown): PlanGateEnvelope | undefined { - if (typeof body !== "object" || body === null) return undefined; - const error = (body as { readonly error?: unknown }).error; - if (typeof error !== "object" || error === null) return undefined; - const code = readString(error, "code"); - const feature = readString(error, "feature"); - const upgradeUrl = readString(error, "upgrade_url"); - if (code !== "entitlement_required" || feature === "" || upgradeUrl === "") { - return undefined; - } - return { feature, upgradeUrl }; -} - function orgSlugFromUpgradeUrl(upgradeUrl: string): string { const match = /\/org\/([^/]+)/.exec(upgradeUrl); return match?.[1] ?? ""; @@ -138,9 +112,9 @@ export const suggestUpgrade = Effect.fn("UpgradeSuggest.check")(function* (opts: const envelope = Option.isSome(body) ? parsePlanGateEnvelope(body.value) : undefined; if (envelope !== undefined) { gate = { - billingUrl: envelope.upgradeUrl, + billingUrl: envelope.upgrade_url, feature: envelope.feature, - orgSlug: orgSlugFromUpgradeUrl(envelope.upgradeUrl), + orgSlug: orgSlugFromUpgradeUrl(envelope.upgrade_url), }; } } diff --git a/apps/cli/src/commands/branches/branches.errors.ts b/apps/cli/src/commands/branches/branches.errors.ts index 293e7722c5..49d3dba3c4 100644 --- a/apps/cli/src/commands/branches/branches.errors.ts +++ b/apps/cli/src/commands/branches/branches.errors.ts @@ -6,6 +6,7 @@ import { ErrorActionabilityId, statusCodeActionability, } from "../../shared/telemetry/error-actionability.ts"; +import type { PlanGateEntitlement } from "../../shared/output/normalize-error.ts"; export class BranchesListNetworkError extends Data.TaggedError("BranchesListNetworkError")<{ readonly message: string; @@ -48,6 +49,7 @@ export class BranchesCreateUnexpectedStatusError extends Data.TaggedError( readonly body: string; readonly message: string; readonly upgradeSuggested?: boolean; + readonly entitlement?: PlanGateEntitlement; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { // A non-gated 409 means the branch name already exists (user input, not a raw API status); @@ -184,6 +186,7 @@ export class BranchesUpdateUnexpectedStatusError extends Data.TaggedError( readonly body: string; readonly message: string; readonly upgradeSuggested?: boolean; + readonly entitlement?: PlanGateEntitlement; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return statusCodeActionability(this.status, { diff --git a/apps/cli/src/commands/branches/create/create.handler.ts b/apps/cli/src/commands/branches/create/create.handler.ts index d268599387..7d6d70b973 100644 --- a/apps/cli/src/commands/branches/create/create.handler.ts +++ b/apps/cli/src/commands/branches/create/create.handler.ts @@ -115,6 +115,7 @@ export const branchesCreate = Effect.fn("branches.create")(function* (flags: Bra body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }), ), ), diff --git a/apps/cli/src/commands/branches/create/create.integration.test.ts b/apps/cli/src/commands/branches/create/create.integration.test.ts index 3b7a4643d7..e592609af6 100644 --- a/apps/cli/src/commands/branches/create/create.integration.test.ts +++ b/apps/cli/src/commands/branches/create/create.integration.test.ts @@ -28,6 +28,7 @@ import { branchesCreateCommand, type BranchesCreateFlags } from "./create.comman import { branchesCreate } from "./create.handler.ts"; import { classifyCliCauseActionability } from "../../../shared/telemetry/error-actionability.ts"; import { textCliOutputFormatter } from "../../../shared/output/text-formatter.ts"; +import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts"; type CreatedBranch = typeof V1CreateABranchOutput.Type; @@ -487,6 +488,32 @@ describe("branches create integration", () => { }).pipe(Effect.provide(layer)); }); + it.effect("carries the envelope's entitlement on the JSON-mode failure", () => { + const upgradeUrl = "https://supabase.com/dashboard/org/env-org/billing"; + const { layer, out } = setup({ + format: "json", + status: 402, + response: { + message: "Branching is supported only on the Pro plan or above", + error: { + code: "entitlement_required", + feature: "branching_limit", + upgrade_url: upgradeUrl, + }, + }, + }); + return Effect.gen(function* () { + yield* branchesCreate({ ...baseFlags, name: Option.some("feat-x") }).pipe( + withJsonErrorHandling, + ); + expect(out.failures).toHaveLength(1); + expect(out.failures[0]).toMatchObject({ + code: "BranchesCreateUnexpectedStatusError", + entitlement: { feature: "branching_limit", upgrade_url: upgradeUrl }, + }); + }).pipe(Effect.provide(layer)); + }); + it.effect("does NOT fire upgrade suggested on 500 (Go skips 5xx)", () => { const { layer, analytics } = setup({ status: 500 }); return Effect.gen(function* () { diff --git a/apps/cli/src/commands/branches/unpause/unpause.integration.test.ts b/apps/cli/src/commands/branches/unpause/unpause.integration.test.ts index 46b20bace2..edc60a59c7 100644 --- a/apps/cli/src/commands/branches/unpause/unpause.integration.test.ts +++ b/apps/cli/src/commands/branches/unpause/unpause.integration.test.ts @@ -2,6 +2,7 @@ import { type V1ListAllProjectsOutput } from "@supabase/api/effect"; import { describe, expect, it } from "@effect/vitest"; import { Cause, Effect, Exit, FileSystem, Option, Schema } from "effect"; +import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts"; import { mockOutput, mockTty } from "../../../../tests/helpers/mocks.ts"; import { VALID_REF, @@ -156,6 +157,36 @@ describe("branches unpause integration", () => { }).pipe(Effect.provide(layer)); }); + it.effect("carries the plan-gate entitlement on a JSON-mode gated restore", () => { + const upgradeUrl = "https://supabase.com/dashboard/org/acme/billing"; + const out = mockOutput({ format: "json" }); + const api = mockCommandPlatformApi({ + handler: (request) => + Effect.sync(() => + jsonResponse(request, 402, { + message: "Restoring this project requires a paid plan", + error: { + code: "entitlement_required", + feature: "project_restore_size", + upgrade_url: upgradeUrl, + }, + }), + ), + }); + const cliSettings = mockCommandSettings({ workdir: tempRoot.current }); + const layer = buildTestRuntime({ out, api, cliSettings }); + return Effect.gen(function* () { + yield* branchesUnpause({ ...baseFlags, name: Option.some(BRANCH_REF) }).pipe( + withJsonErrorHandling, + ); + expect(out.failures).toHaveLength(1); + expect(out.failures[0]).toMatchObject({ + code: "BranchesUnpauseUnexpectedStatusError", + entitlement: { feature: "project_restore_size", upgrade_url: upgradeUrl }, + }); + }).pipe(Effect.provide(layer)); + }); + it.effect( "unpauses the branch chosen from the pickers and fills the real linked-project cache", () => { diff --git a/apps/cli/src/commands/branches/update/update.handler.ts b/apps/cli/src/commands/branches/update/update.handler.ts index b07c87874b..735b1cbf3b 100644 --- a/apps/cli/src/commands/branches/update/update.handler.ts +++ b/apps/cli/src/commands/branches/update/update.handler.ts @@ -74,6 +74,7 @@ export const branchesUpdate = Effect.fn("branches.update")(function* (flags: Bra body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }), ), ), diff --git a/apps/cli/src/commands/sso/add/add.handler.ts b/apps/cli/src/commands/sso/add/add.handler.ts index 7ae676619d..8356a045fc 100644 --- a/apps/cli/src/commands/sso/add/add.handler.ts +++ b/apps/cli/src/commands/sso/add/add.handler.ts @@ -113,6 +113,7 @@ const handleAddError = ( body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), diff --git a/apps/cli/src/commands/sso/list/list.handler.ts b/apps/cli/src/commands/sso/list/list.handler.ts index 53a38f4f3c..d3f8f7ada7 100644 --- a/apps/cli/src/commands/sso/list/list.handler.ts +++ b/apps/cli/src/commands/sso/list/list.handler.ts @@ -52,6 +52,7 @@ const handleListError = (ref: string, cause: SupabaseApiError) => body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), diff --git a/apps/cli/src/commands/sso/remove/remove.handler.ts b/apps/cli/src/commands/sso/remove/remove.handler.ts index a14198767d..455129c8e6 100644 --- a/apps/cli/src/commands/sso/remove/remove.handler.ts +++ b/apps/cli/src/commands/sso/remove/remove.handler.ts @@ -49,6 +49,7 @@ const handleRemoveError = (ref: string, providerId: string, cause: SupabaseApiEr body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), diff --git a/apps/cli/src/commands/sso/sso.errors.ts b/apps/cli/src/commands/sso/sso.errors.ts index a826f877bb..9b09dc43de 100644 --- a/apps/cli/src/commands/sso/sso.errors.ts +++ b/apps/cli/src/commands/sso/sso.errors.ts @@ -6,6 +6,7 @@ import { planLimitGatedActionability, statusCodeActionability, } from "../../shared/telemetry/error-actionability.ts"; +import type { PlanGateEntitlement } from "../../shared/output/normalize-error.ts"; import type { SsoFileErrorReason } from "./sso.saml.ts"; function ssoFileActionability(reason: SsoFileErrorReason): CliErrorActionabilityDeclaration { @@ -85,6 +86,7 @@ export class SsoListUnexpectedStatusError extends Data.TaggedError("SsoListUnexp readonly body: string; readonly message: string; readonly upgradeSuggested?: boolean; + readonly entitlement?: PlanGateEntitlement; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return statusCodeActionability(this.status, { upgradeSuggested: this.upgradeSuggested }); @@ -116,6 +118,7 @@ export class SsoAddUnexpectedStatusError extends Data.TaggedError("SsoAddUnexpec readonly body: string; readonly message: string; readonly upgradeSuggested?: boolean; + readonly entitlement?: PlanGateEntitlement; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return statusCodeActionability(this.status, { upgradeSuggested: this.upgradeSuggested }); @@ -287,6 +290,7 @@ export class SsoUpdateUnexpectedStatusError extends Data.TaggedError( readonly body: string; readonly message: string; readonly upgradeSuggested?: boolean; + readonly entitlement?: PlanGateEntitlement; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return statusCodeActionability(this.status, { @@ -343,6 +347,7 @@ export class SsoRemoveUnexpectedStatusError extends Data.TaggedError( readonly body: string; readonly message: string; readonly upgradeSuggested?: boolean; + readonly entitlement?: PlanGateEntitlement; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return statusCodeActionability(this.status, { upgradeSuggested: this.upgradeSuggested }); diff --git a/apps/cli/src/commands/sso/update/update.handler.ts b/apps/cli/src/commands/sso/update/update.handler.ts index 7927a86314..623fb606d9 100644 --- a/apps/cli/src/commands/sso/update/update.handler.ts +++ b/apps/cli/src/commands/sso/update/update.handler.ts @@ -160,6 +160,7 @@ const handleGetError = (gate: GateContext, providerId: string, cause: Supa body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), @@ -175,6 +176,7 @@ const handlePutError = (gate: GateContext, cause: SupabaseApiError) => body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), diff --git a/apps/cli/src/commands/vanity-subdomains/activate/activate.handler.ts b/apps/cli/src/commands/vanity-subdomains/activate/activate.handler.ts index c92ee4dd27..b7e11002b3 100644 --- a/apps/cli/src/commands/vanity-subdomains/activate/activate.handler.ts +++ b/apps/cli/src/commands/vanity-subdomains/activate/activate.handler.ts @@ -79,6 +79,7 @@ export const vanitySubdomainsActivate = Effect.fn("vanity-subdomains.activate")( body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), diff --git a/apps/cli/src/commands/vanity-subdomains/check-availability/check-availability.handler.ts b/apps/cli/src/commands/vanity-subdomains/check-availability/check-availability.handler.ts index 6b1309ff1f..37c9e18d98 100644 --- a/apps/cli/src/commands/vanity-subdomains/check-availability/check-availability.handler.ts +++ b/apps/cli/src/commands/vanity-subdomains/check-availability/check-availability.handler.ts @@ -83,6 +83,7 @@ export const vanitySubdomainsCheckAvailability = Effect.fn("vanity-subdomains.ch body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), diff --git a/apps/cli/src/commands/vanity-subdomains/vanity-subdomains.errors.ts b/apps/cli/src/commands/vanity-subdomains/vanity-subdomains.errors.ts index 5133a95f6a..9f42ef2276 100644 --- a/apps/cli/src/commands/vanity-subdomains/vanity-subdomains.errors.ts +++ b/apps/cli/src/commands/vanity-subdomains/vanity-subdomains.errors.ts @@ -5,6 +5,7 @@ import { ErrorActionabilityId, statusCodeActionability, } from "../../shared/telemetry/error-actionability.ts"; +import type { PlanGateEntitlement } from "../../shared/output/normalize-error.ts"; /** * Raised by the `activate` and `check-availability` handlers when @@ -69,6 +70,7 @@ export class VanitySubdomainsCheckUnexpectedStatusError extends Data.TaggedError readonly body: string; readonly message: string; readonly upgradeSuggested?: boolean; + readonly entitlement?: PlanGateEntitlement; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return statusCodeActionability(this.status, { @@ -98,6 +100,7 @@ export class VanitySubdomainsActivateUnexpectedStatusError extends Data.TaggedEr readonly body: string; readonly message: string; readonly upgradeSuggested?: boolean; + readonly entitlement?: PlanGateEntitlement; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return statusCodeActionability(this.status, { diff --git a/apps/cli/src/shared/output/normalize-error.ts b/apps/cli/src/shared/output/normalize-error.ts index 3ed617daac..439a61461e 100644 --- a/apps/cli/src/shared/output/normalize-error.ts +++ b/apps/cli/src/shared/output/normalize-error.ts @@ -6,11 +6,18 @@ import { stripControlSequences } from "./strip-control-sequences.ts"; import type { CliErrorSuggestionContext } from "../cli/subcommand-flag-suggestions.ts"; import { formatCliErrorsForDisplay } from "../cli/subcommand-flag-suggestions.ts"; +/** The plan-gate signal machine-mode errors carry, mirroring the Management API's `entitlement_required` body. */ +export type PlanGateEntitlement = { + readonly feature: string; + readonly upgrade_url: string; +}; + type NormalizedCliError = { readonly code: string; readonly message: string; readonly detail?: string; readonly suggestion?: string; + readonly entitlement?: PlanGateEntitlement; }; type ErrorRecord = Record; @@ -32,6 +39,14 @@ const readRawString = (value: ErrorRecord, key: string): string | undefined => { return typeof field === "string" ? field : undefined; }; +const readEntitlement = (error: ErrorRecord): PlanGateEntitlement | undefined => { + const entitlement = error["entitlement"]; + if (!isErrorRecord(entitlement)) return undefined; + const feature = readString(entitlement, "feature"); + const upgradeUrl = readString(entitlement, "upgrade_url"); + return feature && upgradeUrl ? { feature, upgrade_url: upgradeUrl } : undefined; +}; + /** A config parse failure's reason and position, without the source text it quotes, which can hold secrets. */ export function configParseCauseMessage(cause: unknown): string { const text = cause instanceof Error ? cause.message : String(cause); @@ -161,11 +176,13 @@ export function normalizeCliError( // whitespace (e.g. `suggestLegacyBundle`'s leading `\n` for a blank // separator line); `readString` would trim exactly that away. const suggestion = readRawString(error, "suggestion"); + const entitlement = readEntitlement(error); return { code, message, ...(detail && detail !== message ? { detail } : {}), ...(suggestion !== undefined && suggestion.length > 0 ? { suggestion } : {}), + ...(entitlement === undefined ? {} : { entitlement }), }; } diff --git a/apps/cli/src/shared/output/output.layer.ts b/apps/cli/src/shared/output/output.layer.ts index 67f28e2fff..bab2a0de97 100644 --- a/apps/cli/src/shared/output/output.layer.ts +++ b/apps/cli/src/shared/output/output.layer.ts @@ -30,6 +30,7 @@ import { import { Tty } from "../runtime/tty.service.ts"; import { CONTEXT_CANCELED_MESSAGE, NonInteractiveError, StdoutClosedError } from "./errors.ts"; import { MachineErrorContext } from "./machine-error-context.service.ts"; +import type { PlanGateEntitlement } from "./normalize-error.ts"; import { Output } from "./output.service.ts"; import type { OutputFormat, StreamEvent } from "./types.ts"; @@ -498,7 +499,13 @@ export const textOutputLayer = Layer.effect( }), result: () => Effect.void, success: (message: string) => logAround(log.success, message), - fail: (err: { code: string; message: string; detail?: string; suggestion?: string }) => + fail: (err: { + code: string; + message: string; + detail?: string; + suggestion?: string; + entitlement?: PlanGateEntitlement; + }) => Effect.sync(() => { // A command failure is terminal, so a still-shown task spinner is dropped. if (activeSpinner !== undefined) { @@ -593,7 +600,13 @@ export const jsonOutputLayer = Layer.effect( }), result, success: (message: string, data?: Record) => result({ ...data, message }), - fail: (err: { code: string; message: string; detail?: string; suggestion?: string }) => + fail: (err: { + code: string; + message: string; + detail?: string; + suggestion?: string; + entitlement?: PlanGateEntitlement; + }) => Effect.gen(function* () { const extra = yield* readMachineErrorContext(); // `extra` spreads first so the envelope's own `_tag`/`error` can't @@ -682,7 +695,13 @@ export const streamJsonOutputLayer = Layer.effect( }), result, success: (message: string, data?: Record) => result({ ...data, message }), - fail: (err: { code: string; message: string; detail?: string; suggestion?: string }) => + fail: (err: { + code: string; + message: string; + detail?: string; + suggestion?: string; + entitlement?: PlanGateEntitlement; + }) => Effect.gen(function* () { const extra = yield* readMachineErrorContext(); // `extra` spreads first so the event's own `type`/`error`/`timestamp` diff --git a/apps/cli/src/shared/output/output.service.ts b/apps/cli/src/shared/output/output.service.ts index eb0668a11d..716ffecd6b 100644 --- a/apps/cli/src/shared/output/output.service.ts +++ b/apps/cli/src/shared/output/output.service.ts @@ -2,6 +2,7 @@ import type { Effect } from "effect"; import { Context } from "effect"; import type { NonInteractiveError } from "./errors.ts"; +import type { PlanGateEntitlement } from "./normalize-error.ts"; import type { OutputFormat, StreamEvent } from "./types.ts"; export interface OutputTask { @@ -90,6 +91,7 @@ interface OutputShape { readonly message: string; readonly detail?: string; readonly suggestion?: string; + readonly entitlement?: PlanGateEntitlement; }) => Effect.Effect; /** * Writes a raw chunk to stdout or stderr without framing. From e3d4723e1ebce19f057c733b6c808099a4e359c6 Mon Sep 17 00:00:00 2001 From: Isaac Dawson Date: Mon, 26 Oct 2026 08:20:39 +0000 Subject: [PATCH 12/33] docs: Add SECURITY.md (#7322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds a SECURITY.md file so we can get better signal on our security reports. ## Linked issue [Linear Issue](https://linear.app/supabase/issue/SEC-993/securitymd-to-guide-researchers) - [x] The linked issue is **open** and carries the `open-for-contribution` label (or I'm a Supabase maintainer). ## Checklist - [x] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/) (e.g. `fix(cli): …`). - [ ] Tests added or updated for the change. - [ ] From the repository root, `pnpm check:all` passes; relevant package tests pass for every touched workspace, and `pnpm types:check` passes for each touched TypeScript workspace (or workspace declaring it). --------- Co-authored-by: Claude Sonnet 5.5 --- SECURITY.md | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..01b10a04e0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,232 @@ +# Security Policy + +Thank you for helping keep the Supabase CLI and its users safe. + +## Reporting a vulnerability + +Please report suspected vulnerabilities through our HackerOne program: +. Please do not open public GitHub issues or +pull requests for security problems. + +This repository contains the Supabase CLI. It runs on a developer's machine or +in CI, starts the local development stack (`supabase start`), manages +migrations, deploys and downloads Edge Functions, and talks to the hosted +Supabase Management API on the user's behalf. There is no hosted CLI: the +target is the CLI and the local stack it starts on your own machine. Issues in +the hosted Management API itself are not tracked in this repository. Report +them through HackerOne as usual and say which API you tested. + +## What we consider a vulnerability + +In general, we want to hear about anything that lets an attacker get a CLI +user's credentials, act as that user, or run code or write files on their +machine without the user choosing to: obtaining a Supabase access token or +project secret the attacker should not have, redirecting the CLI's +credentials to a server the user did not choose, connecting to a database or +API without properly verifying who is on the other end, or turning data that +the CLI downloads into a write outside the location it was meant for. The +attacker should not need the victim's credentials or a shell on the victim's +machine, and the problem should be present in a released version of the CLI +used as documented. + +Examples of what this includes (these are illustrations, not a complete list): + +- **Theft of a CLI access token.** Getting a user to complete a CLI login, or + any other flow, in a way that hands the resulting access token to the + attacker. +- **Writes outside the intended directory.** Content the CLI downloads (for + example a function bundle or its file paths) that makes the CLI create or + overwrite files outside the project directory it was asked to write to. Show + the file actually written to a location of the attacker's choosing. +- **Secret exposure.** An access token, database password or project secret + key written to output, logs or local files in a place where the CLI is meant + to redact or omit it. +- **Credentials sent to the wrong place.** Project or repository content that + the CLI reads, such as a `.env` file or `config.toml`, that changes where the + CLI sends its access token or which server it talks to, without the user + choosing that. +- **Server identity not verified.** A CLI connection that carries credentials or + data, such as to the project database or the Management API, that does not + verify the server's TLS certificate and hostname, or that silently accepts a + weaker mode than the user asked for. This is in scope even though exploiting + it needs a network position between the user and the server: the CLI is + expected to protect those connections, so it is not the MITM exclusion + below. Show which command connects, what the attacker intercepts, and the + CLI version. + +## What we do not consider a vulnerability + +Reports that rely on the behavior below will normally be closed as +informative or not applicable. + +**By design** + +- **The local development stack is for your own workstation.** The stack that + `supabase start` runs is meant for local development. It has no + authorization or isolation layer suitable for the internet, and our + documentation says it should not be exposed publicly. Reports against a + local stack that someone has made reachable from the internet are reports + about that deployment, not about the CLI. If you believe the same issue + affects a self-hosted deployment, reproduce it using the self-hosting guide + and say so. +- **The CLI uses credentials that are already present.** If an access token is + set in the environment (`SUPABASE_ACCESS_TOKEN`) or saved by an earlier + `supabase login`, commands that call the Management API will use it without + prompting. A command that "works without logging in" on a machine or agent + session that already holds a token is not an authentication bypass. Retest in + a clean environment. +- **The CLI acts with its user's privileges.** Running SQL, pushing + migrations, deploying functions or managing secrets for projects the token + can reach is the purpose of the tool. + +**Prerequisites that make the attack pointless** + +- The attacker needs the victim's access token, database password or project + secret key. +- The attacker already has a shell, or write access to the user's home + directory, on the victim's machine. +- The attacker needs owner or admin rights on the victim's project or + organization, and the action is one those rights already allow. + +**Reports that are not evidence of a flaw** + +- **Credentials in the git history of this repository** that are revoked or no + longer valid, including values in old or commented-out config templates. + Show that a credential is live and grants access. +- **Static analysis or scanner output** with no working proof of concept + against a released CLI version. + +## Out of scope for every Supabase asset + +These are excluded program-wide (see also +). They are repeated here so +you do not need to leave this repository to find them. + +- Clickjacking on pages with no sensitive actions. +- Unauthenticated, logout and login CSRF. +- Attacks requiring MITM or physical access to a user's device. +- Attacks requiring social engineering. +- Any activity that could lead to the disruption of our service (DoS). + This means volumetric attacks, load testing and flooding. A vulnerability in + our code that lets an unprivileged attacker crash a service or exhaust + resources for other users or tenants is in scope. Denial of service that only + affects the attacker's own project, or that needs admin or owner privileges, + is not. +- Content spoofing and text injection issues without showing an attack vector, + or without being able to modify HTML/CSS. +- Email spoofing. +- Missing DNSSEC, CAA or CSP headers. +- Lack of Secure or HttpOnly flags on non-sensitive cookies. +- Dead links. +- User enumeration. + +## Known and documented behavior + +These are described in our public documentation. Please check them before +reporting. + +- **Local development is not self-hosting.** The local development guide and + the self-hosting guide describe different setups. The local stack is not + intended to be exposed outside the developer's machine. +- **`SUPABASE_ACCESS_TOKEN`** can be set in the environment to authenticate the + CLI without an interactive login, for example in CI. + +## Local development stack vs. custom setups + +We accept findings against released CLI versions used as documented. We treat +findings that need the user to misconfigure their environment first, such as by +exposing the local stack to a network, changing its `config.toml` defaults, or +running a modified CLI, as lower severity. Label your report as described in +the next section. + +## How to write a report that gets triaged quickly + +- **Say what you tested, at the top of the report and in the title.** Use one of + these labels, for example `[Local CLI] ...` in the title and a "Target" line + at the start of the description: + - **Local CLI, default configuration**: a released CLI version, and the local + stack it starts, used exactly as our documentation describes with the + default `config.toml`. Include the CLI version, how you installed it, and + your operating system. + - **Local CLI, custom configuration**: you changed `config.toml`, exposed the + local stack beyond your machine, built from a branch, or modified code. List + every change and why. We triage these lower unless the setting is one we + recommend or document. + + Reports that do not say which of the two they are about are slower to + triage, and are often closed with a request to retest. A finding that works + only on a custom configuration is not a finding against a default install. + +- **Test a real setup.** Run the released CLI binary, not only code you copied + out of it. Code reading alone is rarely enough. Static analysis findings need + a working proof of concept, and the code you cite must match the current + repository. +- **Give a short, runnable PoC.** The exact commands, any files or server + responses the attacker controls, and the output showing the impact, such as + the file written or the token received. +- **State the attacker's starting position.** What do they control (a function + in an organization the victim belongs to, a web page, a leaked key)? What + does the victim have to run? Reports that quietly assume a token already in + the environment, or a shell on the victim's machine, are our most common + reason to close. +- **Name the boundary you crossed.** Which user, project or organization could + not do this before, or which location could the attacker not write to? If + nothing was crossed, it is probably a hardening suggestion. +- **Check the docs and this file first.** If the behavior is documented, explain + why the documented design still fails. +- **Include versions.** The CLI version, installation method, operating system, + and whether any `SUPABASE_*` environment variables were set. +- **One issue per report.** Link variants of the same root cause instead of + filing each variant separately. +- **Do not send unreviewed AI output.** Reports that were not reproduced by a + human and that ignore the documentation are closed. AI-assisted analysis is + fine when you have verified the result. If an AI agent ran the CLI for you, + say whether it had logged in or had a token. +- **Respond to follow-up questions.** Reports that go quiet after we ask for a + reproduction are closed. + +## Severity + +We rate severity by realistic impact on a default setup. Issues that need user +interaction the user can see, a deliberately exposed local stack, or an +existing privilege are rated lower even when they are valid, and we may fix +them as defense in depth. + +## Testing and disclosure + +These are our program-wide rules. They also apply to AI agents and automated +tools acting on your behalf. + +**Testing** + +- Do not run automated scanners against other customers' projects. Scanners can + run up costs for our users, can disrupt services, and our own security + systems cannot tell hostile reconnaissance from research. If you want to run + an automated scanner, notify security@supabase.com first and run it only on + your own Supabase project. +- Do not attack projects that belong to other customers. +- Do not take advantage of a vulnerability you find, for example by downloading + more data than necessary to demonstrate it, or by deleting or modifying other + people's data. + +**Disclosure** + +- Do not reveal the problem to others until we have researched and fixed it and + informed affected customers. +- If you want to share your research publicly (a conference, a blog or any + other public forum), send us a draft for review and approval at least 30 days + before the publication date. Do not include data about Supabase customer + projects, customers' data, or information about Supabase employees, + contractors or partners. + +**What we promise** + +- We will respond to your report within 5 business days with our evaluation and + an expected resolution date. +- If you have followed these instructions, we will not take legal action against + you in regard to the report. +- We will handle your report with strict confidentiality and will not pass on + your personal details to third parties without your permission. +- We will keep you informed of our progress. +- In public information about the problem, we will name you as the discoverer + unless you prefer otherwise. From 249b048560319d8f5b7df65f7044b7de9b548702 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:26:59 +0000 Subject: [PATCH 13/33] chore(stack): bump postgres to 17.11.0.007-r0 (#7293) This bumps postgres from 17.11.0.006-r0 to 17.11.0.007-r0. https://github.com/supabase/slim-services/releases/tag/postgres-17.11.0.007-r0 Planned after supabase/slim-services published 17.11.0.007-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 39121e0d70..85a59241a9 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -5,7 +5,7 @@ # a catalog update. Everything else in this file (this comment, kong, pg14, the one-shot job # images) is hand- or Dependabot-managed. Keep a line between image lines: git conflicts on edits to # adjacent lines, and catalog PRs for neighbouring aliases are often open at the same time. -FROM supabase/postgres:17.11.0.006 AS pg +FROM supabase/postgres:17.11.0.007 AS pg FROM supabase/postgres:15.19.0.007 AS pg15 # Postgres 14 has no slim build; bumped by hand (Dependabot ignores supabase/postgres). diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 39121e0d70..85a59241a9 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -5,7 +5,7 @@ # a catalog update. Everything else in this file (this comment, kong, pg14, the one-shot job # images) is hand- or Dependabot-managed. Keep a line between image lines: git conflicts on edits to # adjacent lines, and catalog PRs for neighbouring aliases are often open at the same time. -FROM supabase/postgres:17.11.0.006 AS pg +FROM supabase/postgres:17.11.0.007 AS pg FROM supabase/postgres:15.19.0.007 AS pg15 # Postgres 14 has no slim build; bumped by hand (Dependabot ignores supabase/postgres). diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 7925711fb1..4240f019c2 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -127,23 +127,23 @@ const definitions: Readonly> = { database: definition( "postgres", { - upstreamVersion: "17.11.0.006", + upstreamVersion: "17.11.0.007", revision: 0, image: - "ghcr.io/supabase/cli/postgres:17.11.0.006-r0@sha256:ee0f2d9747fb9d3c0a29703684ec718f070384ad05bf0d92823ed02cabde8e5a", - upstreamImage: "supabase/postgres:17.11.0.006", + "ghcr.io/supabase/cli/postgres:17.11.0.007-r0@sha256:320b6fad223dfb8ff40a1893936232d34cd47c0f4f807e79741017ac3c1974a2", + upstreamImage: "supabase/postgres:17.11.0.007", natives: { "darwin-arm64": { - archive: "fb7594525d712ab85b6da00d4d4854f8d0c9ba26b49b781a5fede0119362e917", - manifest: "b499127a253b15f362fa217301d490f5d61f4b201d1b6501de06a11c6347e192", + archive: "ad0f293a750ea2e2d0c4f2c342a703d923fa1d3da3b1e1427722f760f2520cbc", + manifest: "e3998a69465ccc774d5a938b48d35b4e720f51557cab69358228502c4e1093fc", }, "linux-amd64": { - archive: "f3a4389c92c550597cacf8c71c74af025b8e75259753c4e78bb5014e6f29e6f2", - manifest: "457949debc6c8b1d17dfaedcdef4883af3ff1ef7a15998f74d9511294be9a2fa", + archive: "0496408934df764c3a90a83aa49599cb374eab638f85cc8a5d45f07e965d0812", + manifest: "789d555736087fb7699c75c9047272e41021f9813ecc47d453c0ab59d4bc0ce3", }, "linux-arm64": { - archive: "443bb6be464b78fcc9a1e07d94cc4e8a5f0f9cba3d4ca3d5d8a8563fa962fddb", - manifest: "fd0f71d0f42898657a69962475164f4446f8f732edfac248708593b36eec4fcf", + archive: "b16c21eb7b86aee84ceadc0834f4639de851a0b56244984057a1d299fb54583c", + manifest: "6e91bb3eba59bf5e9c31b6a5ce66d66a1cbaa87f3bbfc41542e0c540c2d6303d", }, }, }, From 94174764efd45a0cd43ff49e0fb3f99b09cfd5c5 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:53:23 +0000 Subject: [PATCH 14/33] chore(stack): bump realtime to v2.147.4-r0 (#7328) This bumps realtime from v2.147.3-r0 to v2.147.4-r0. https://github.com/supabase/slim-services/releases/tag/realtime-v2.147.4-r0 Planned after supabase/slim-services published v2.147.4-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 85a59241a9..12dae979be 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.147.3 AS realtime +FROM supabase/realtime:v2.147.4 AS realtime FROM supabase/storage-api:v1.84.23 AS storage diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 85a59241a9..12dae979be 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.147.3 AS realtime +FROM supabase/realtime:v2.147.4 AS realtime FROM supabase/storage-api:v1.84.23 AS storage diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 4240f019c2..f242fe917d 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -317,23 +317,23 @@ const definitions: Readonly> = { realtime: definition( "realtime", { - upstreamVersion: "v2.147.3", + upstreamVersion: "v2.147.4", revision: 0, image: - "ghcr.io/supabase/cli/realtime:v2.147.3-r0@sha256:a2c714566a075fa9f39eeea88d659b8aba5fedaddd80755bc670c6c9356de5c0", - upstreamImage: "supabase/realtime:v2.147.3", + "ghcr.io/supabase/cli/realtime:v2.147.4-r0@sha256:70e5f3099579b5e2ee422152ab3d30dd74e3205370edbbf9927a50c2d06749a7", + upstreamImage: "supabase/realtime:v2.147.4", natives: { "darwin-arm64": { - archive: "1cd9a606c9b73003deef6b41b308fe7401e22204f019fe9c3d6fec384eccb72d", - manifest: "37d1cbc4e48c0d934d183504369b22fa0c8101f6ffddd38b7e45f0314590cbee", + archive: "f692c6c64ac10e94e2b2ac911b57739207a63109359078c8755f77b0f2c74513", + manifest: "d989b06735a2e72b5a40a3e08714577ba139892e68f20602aeecae53b22ffd42", }, "linux-amd64": { - archive: "0f48d7a74782e9ea55ea57f77599a6a87978862f573e3a1acb20fe17426b0933", - manifest: "e235af418fafcbe44c1d5e884e79cfe868b3a152a3159649b99fc1793f614397", + archive: "65d92c29ad286cdb6218a9bf26541ac48a1fbf5726cc31af0a06e895d5086558", + manifest: "8b2cd04ed1381a38dd578a42c200698c7aff190f5e554f2ea7d79da45bad2194", }, "linux-arm64": { - archive: "153f356b6008de522927b4204a4b4ca9a5389870bcb8591063f8228cad79cada", - manifest: "11c29d1e86f006b0475d622f0261d4deaab8754d779183b41eea099c514c51f1", + archive: "a3e6fab857e1d51a3cc0f9a088eb0b2e4a90b300269346c4d26f36d45b495c37", + manifest: "46624b79879156579886bc1508e10ff6f414e6fce731681a0f906a7b0875dcf3", }, }, }, From 3b098d86bd1c3eaa38b70b70b05e99e1b89b1d7e Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:50:09 +0000 Subject: [PATCH 15/33] fix(cli): skip reserved-role parameter grants in role dump (CLI-2843) (#7320) ## TL;DR fixes restoring a `supabase db dump --role-only` file as `postgres` failing on a parameter grant to a reserved role ## whats improved/fixed? `supabase db dump --role-only` already comments out create, alter and membership grant lines for reserved roles, but parameter grants like `GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";` passed through linked and local projects whose realtime setup grants this get that line in `roles.sql`, so restoring it as `postgres` aborted with `permission denied for parameter log_min_messages` ## how? a new rule anchored to the start of the line comments out parameter grants to reserved roles, in both the dump script and the in-process filter the local stack uses grants to other roles and grant text inside role setting values stay as they are ## ref: - closes: https://github.com/supabase/cli/issues/7315 --- .../pg-dump.run.integration.test.ts | 401 +++++++++++++----- apps/cli/src/command-internal/pg-dump.run.ts | 31 +- .../src/command-internal/pg-dump.scripts.ts | 2 +- 3 files changed, 319 insertions(+), 115 deletions(-) diff --git a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts index 37b85cfeb6..469e0199b2 100644 --- a/apps/cli/src/command-internal/pg-dump.run.integration.test.ts +++ b/apps/cli/src/command-internal/pg-dump.run.integration.test.ts @@ -1,8 +1,11 @@ import { describe, expect, it } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; -import { Effect, Layer, Option } from "effect"; +import { Effect, Layer, Option, Stream } from "effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; import type { Stack } from "@supabase/stack/effect"; import { streamPgDumpWithClient, transformManagedDumpLine } from "./pg-dump.run.ts"; +import { dumpRoleScript } from "./pg-dump.scripts.ts"; +import { expandScript } from "./pg-dump.env.ts"; import { NetworkIdFlag } from "./global-flags.ts"; import { DockerRun } from "./docker-run.service.ts"; import { BundledPostgresClient } from "./bundled-postgres-client.ts"; @@ -10,7 +13,7 @@ import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; import { selection } from "../../tests/helpers/pins.ts"; import { mockOutput } from "../../tests/helpers/mocks.ts"; -const stackFixture = (output: string) => { +const stackFixture = (output: string | ReadonlyArray) => { const calls: Array<{ readonly args: ReadonlyArray; readonly command: string }> = []; const stack = { commands: { @@ -23,86 +26,170 @@ const stackFixture = (output: string) => { ) => Effect.gen(function* () { calls.push({ command: _tool.command, args: options.args ?? [] }); - yield* options.stdout(new TextEncoder().encode(output)); + yield* Effect.forEach( + typeof output === "string" ? [new TextEncoder().encode(output)] : output, + options.stdout, + { discard: true }, + ); return { jobId: "job", exitCode: 0 }; }), }, } as unknown as Stack; - return { stack, calls }; + const layer = Layer.mergeAll( + BunServices.layer, + Layer.succeed(NetworkIdFlag, Option.none()), + Layer.succeed(RuntimeInfo, { + cwd: "/tmp", + platform: "linux", + arch: "x64", + homeDir: "/tmp", + execPath: "/usr/bin/bun", + pid: 1, + }), + Layer.succeed(BundledPostgresClient, { run: () => Effect.die("unused") }), + Layer.succeed(DockerRun, { + run: () => Effect.die("unused"), + runCapture: () => Effect.die("unused"), + runStream: () => Effect.die("unused"), + }), + mockOutput().layer, + ); + return { stack, calls, layer }; }; describe("managed pg_dump runner", () => { - it("keeps the managed dump filters equivalent to the CLI scripts", () => { - const cases = [ - [ - 'CREATE TABLE "public" (id integer);', - "pg_dump --schema-only", - {}, - 'CREATE TABLE IF NOT EXISTS "public" (id integer);', - ], - ["\\unrestrict 123", "pg_dump --data-only", {}, "-- \\unrestrict 123"], - ["--user-data", "pg_dump --data-only", { EXTRA_SED: "/^--/d" }, "--user-data"], - ["\\restrict 123", "pg_dumpall --roles-only", { EXTRA_SED: "/^--/d" }, undefined], - [ - 'CREATE ROLE "postgres";', - "pg_dumpall --roles-only", - { RESERVED_ROLES: "postgres" }, - '-- CREATE ROLE "postgres";', - ], - [ - 'ALTER ROLE "postgres" SET "statement_timeout" TO "5s";', - "pg_dumpall --roles-only", - { ALLOWED_CONFIGS: "statement_timeout" }, - 'ALTER ROLE "postgres" SET "statement_timeout" TO "5s";', - ], - [ - '-- ALTER ROLE "postgres" SET "pgrst.db_schemas" TO "public";', - "pg_dumpall --roles-only", - { ALLOWED_CONFIGS: "pgrst.*", EXTRA_SED: "/^--/d" }, - 'ALTER ROLE "postgres" SET "pgrst.db_schemas" TO "public";', - ], - [ - 'GRANT "member" TO "dashboard_user";', - "pg_dumpall --roles-only", - { RESERVED_ROLES: "dashboard_user", EXTRA_SED: "/^--/d" }, - undefined, - ], - [ - 'CREATE EXTENSION IF NOT EXISTS "pg_tle" WITH SCHEMA "pgtle";', - "pg_dump --schema-only", - {}, - 'CREATE EXTENSION IF NOT EXISTS "pg_tle";', - ], - [ - 'GRANT SELECT ON TABLE "public"."storage" TO "anon";', - "pg_dump --schema-only", - { EXCLUDED_SCHEMAS: "storage" }, - 'GRANT SELECT ON TABLE "public"."storage" TO "anon";', - ], - [ - 'REVOKE SELECT ON TABLE "storage" FROM "anon";', - "pg_dump --schema-only", - { EXCLUDED_SCHEMAS: "storage" }, - '-- REVOKE SELECT ON TABLE "storage" FROM "anon";', - ], - [ - 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "authenticated";', - "pg_dump --schema-only", - {}, - 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "authenticated";', - ], - [ - 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "postgres" WITH GRANT OPTION;', - "pg_dump --schema-only", - {}, - '-- GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "postgres" WITH GRANT OPTION;', - ], - ] as const; + const cases = [ + [ + 'CREATE TABLE "public" (id integer);', + "pg_dump --schema-only", + {}, + 'CREATE TABLE IF NOT EXISTS "public" (id integer);', + ], + ["\\unrestrict 123", "pg_dump --data-only", {}, "-- \\unrestrict 123"], + ["--user-data", "pg_dump --data-only", { EXTRA_SED: "/^--/d" }, "--user-data"], + ["\\restrict 123", "pg_dumpall --roles-only", { EXTRA_SED: "/^--/d" }, undefined], + [ + 'CREATE ROLE "postgres";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "postgres" }, + '-- CREATE ROLE "postgres";', + ], + [ + 'ALTER ROLE "postgres" SET "statement_timeout" TO "5s";', + "pg_dumpall --roles-only", + { ALLOWED_CONFIGS: "statement_timeout" }, + 'ALTER ROLE "postgres" SET "statement_timeout" TO "5s";', + ], + [ + '-- ALTER ROLE "postgres" SET "pgrst.db_schemas" TO "public";', + "pg_dumpall --roles-only", + { ALLOWED_CONFIGS: "pgrst.*", EXTRA_SED: "/^--/d" }, + 'ALTER ROLE "postgres" SET "pgrst.db_schemas" TO "public";', + ], + [ + 'GRANT "member" TO "dashboard_user";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "dashboard_user", EXTRA_SED: "/^--/d" }, + undefined, + ], + [ + 'GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_realtime_admin" }, + '-- GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";', + ], + [ + 'GRANT ALL ON PARAMETER "log_min_messages" TO "supabase_admin" WITH GRANT OPTION;', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_.*" }, + '-- GRANT ALL ON PARAMETER "log_min_messages" TO "supabase_admin" WITH GRANT OPTION;', + ], + [ + 'GRANT SET ON PARAMETER "log_min_messages" TO "app_user";', + "pg_dumpall --roles-only", + { RESERVED_ROLES: "supabase_.*" }, + 'GRANT SET ON PARAMETER "log_min_messages" TO "app_user";', + ], + [ + `ALTER ROLE "app_user" SET "myapp.restore_sql" TO 'GRANT SET ON PARAMETER "log_min_messages" TO "postgres";';`, + "pg_dumpall --roles-only", + { RESERVED_ROLES: "postgres" }, + `ALTER ROLE "app_user" SET "myapp.restore_sql" TO 'GRANT SET ON PARAMETER "log_min_messages" TO "postgres";';`, + ], + [ + 'CREATE EXTENSION IF NOT EXISTS "pg_tle" WITH SCHEMA "pgtle";', + "pg_dump --schema-only", + {}, + 'CREATE EXTENSION IF NOT EXISTS "pg_tle";', + ], + [ + 'GRANT SELECT ON TABLE "public"."storage" TO "anon";', + "pg_dump --schema-only", + { EXCLUDED_SCHEMAS: "storage" }, + 'GRANT SELECT ON TABLE "public"."storage" TO "anon";', + ], + [ + 'REVOKE SELECT ON TABLE "storage" FROM "anon";', + "pg_dump --schema-only", + { EXCLUDED_SCHEMAS: "storage" }, + '-- REVOKE SELECT ON TABLE "storage" FROM "anon";', + ], + [ + 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "authenticated";', + "pg_dump --schema-only", + {}, + 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "authenticated";', + ], + [ + 'GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "postgres" WITH GRANT OPTION;', + "pg_dump --schema-only", + {}, + '-- GRANT ALL ON FOREIGN DATA WRAPPER "wrapper" TO "postgres" WITH GRANT OPTION;', + ], + ] as const; + it("keeps the managed dump filters equivalent to the CLI scripts", () => { for (const [input, script, env, expected] of cases) expect(transformManagedDumpLine(input, script, env)).toBe(expected); }); + it.live.skipIf(process.platform === "win32")("filters role dumps through the CLI script", () => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + for (const [input, script, env, expected] of cases) { + if (!script.includes("pg_dumpall")) continue; + const child = yield* spawner.spawn( + ChildProcess.make( + "bash", + ["-c", `pg_dumpall() { printf '%s\\n' "$DUMP_INPUT"; }\n${dumpRoleScript}`], + { + env: { + PGHOST: "127.0.0.1", + PGPORT: "5432", + PGUSER: "postgres", + PGPASSWORD: "", + PGDATABASE: "postgres", + RESERVED_ROLES: "", + ALLOWED_CONFIGS: "", + EXTRA_SED: "", + ...env, + DUMP_INPUT: input, + }, + stdin: "ignore", + stderr: "inherit", + }, + ), + ); + const [exitCode, stdout] = yield* Effect.all( + [child.exitCode, Stream.mkString(Stream.decodeText(child.stdout))], + { concurrency: "unbounded" }, + ); + expect(exitCode).toBe(0); + expect(stdout).toBe(`${expected === undefined ? "" : `${expected}\n`}RESET ALL;\n`); + } + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); + it.live("keeps data dump filters and quoted extra flags in the Stack tool arguments", () => { const fixture = stackFixture("\\unrestrict 123\n"); return streamPgDumpWithClient({ @@ -118,27 +205,7 @@ describe("managed pg_dump runner", () => { onStdout: () => Effect.void, client: { kind: "stack", stack: fixture.stack, command: "pg_dump", pin: selection.database }, }).pipe( - Effect.provide( - Layer.mergeAll( - BunServices.layer, - Layer.succeed(NetworkIdFlag, Option.none()), - Layer.succeed(RuntimeInfo, { - cwd: "/tmp", - platform: "linux", - arch: "x64", - homeDir: "/tmp", - execPath: "/usr/bin/bun", - pid: 1, - }), - Layer.succeed(BundledPostgresClient, { run: () => Effect.die("unused") }), - Layer.succeed(DockerRun, { - run: () => Effect.die("unused"), - runCapture: () => Effect.die("unused"), - runStream: () => Effect.die("unused"), - }), - mockOutput().layer, - ), - ), + Effect.provide(fixture.layer), Effect.tap(() => Effect.sync(() => { expect(fixture.calls[0]?.args).toContain("--exclude-table"); @@ -176,27 +243,7 @@ describe("managed pg_dump runner", () => { }), client: { kind: "stack", stack: fixture.stack, command: "pg_dump", pin: selection.database }, }).pipe( - Effect.provide( - Layer.mergeAll( - BunServices.layer, - Layer.succeed(NetworkIdFlag, Option.none()), - Layer.succeed(RuntimeInfo, { - cwd: "/tmp", - platform: "linux", - arch: "x64", - homeDir: "/tmp", - execPath: "/usr/bin/bun", - pid: 1, - }), - Layer.succeed(BundledPostgresClient, { run: () => Effect.die("unused") }), - Layer.succeed(DockerRun, { - run: () => Effect.die("unused"), - runCapture: () => Effect.die("unused"), - runStream: () => Effect.die("unused"), - }), - mockOutput().layer, - ), - ), + Effect.provide(fixture.layer), Effect.asVoid, Effect.tap(() => Effect.sync(() => { @@ -212,3 +259,133 @@ describe("managed pg_dump runner", () => { ); }); }); + +describe("role dump parameter grants", () => { + const comment = "-- A comment with an unmatched quote: '\n"; + const settings = `ALTER ROLE "app'role" SET "pgrst.db_schemas" TO 'public'; +ALTER ROLE "app_user" SET "pgrst.multiline_sql" TO 'SELECT 1; +GRANT SET ON PARAMETER "log_min_messages" TO "postgres"; +SELECT 2;'; +ALTER ROLE "app_user" SET "pgrst.escaped_sql" TO E'backslash\\\\''quote'' +GRANT SET ON PARAMETER "log_min_messages" TO "postgres"; +last line'; +`; + const reservedGrant = 'GRANT SET ON PARAMETER "log_min_messages" TO "supabase_realtime_admin";'; + const customGrant = 'GRANT SET ON PARAMETER "log_lock_waits" TO "app_user";'; + const quotedCustomGrant = 'GRANT SET ON PARAMETER "log_lock_waits" TO "postgres""custom";'; + const input = `${comment}${settings}${reservedGrant}\n${customGrant}\n${quotedCustomGrant}\n`; + + for (const keepComments of [false, true]) { + const env = { + PGHOST: "unused", + PGPORT: "5432", + PGUSER: "postgres", + PGPASSWORD: "", + PGDATABASE: "postgres", + RESERVED_ROLES: "postgres|supabase_.*", + ALLOWED_CONFIGS: "pgrst.*", + EXTRA_SED: keepComments ? "" : "/^--/d", + }; + const expected = `${keepComments ? comment : ""}${settings}${keepComments ? `-- ${reservedGrant}\n` : ""}${customGrant}\n${quotedCustomGrant}\nRESET ALL;\n`; + + it.live( + `preserves quoted settings across Stack chunks with keep-comments=${keepComments}`, + () => { + const bytes = new TextEncoder().encode(input); + const fixture = stackFixture( + Array.from({ length: Math.ceil(bytes.length / 7) }, (_, i) => + bytes.slice(i * 7, (i + 1) * 7), + ), + ); + return Effect.gen(function* () { + let output = ""; + const result = yield* streamPgDumpWithClient({ + image: "unused", + script: dumpRoleScript, + env, + onStdout: (chunk) => + Effect.sync(() => { + output += new TextDecoder().decode(chunk); + }), + client: { + kind: "stack", + stack: fixture.stack, + command: "pg_dumpall", + pin: selection.database, + }, + }); + expect(result.exitCode).toBe(0); + expect(output).toBe(expected); + }).pipe(Effect.provide(fixture.layer)); + }, + ); + + describe.skipIf(process.platform === "win32")("Bash script", () => { + const longSetting = `ALTER ROLE "app_user" SET "myapp.large" TO '${"x".repeat(512 * 1024)}';\n`; + for (const dryRun of [false, true]) { + it.live( + `preserves long quoted settings with keep-comments=${keepComments}, dry-run=${dryRun}`, + () => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const script = dryRun ? expandScript(dumpRoleScript, env) : dumpRoleScript; + const child = yield* spawner.spawn( + ChildProcess.make("bash", ["-c", `pg_dumpall() { cat; }\n${script}`], { + env, + stdin: Stream.make(new TextEncoder().encode(`${longSetting}${input}`)), + stderr: "inherit", + }), + ); + const [exitCode, output] = yield* Effect.all( + [child.exitCode, Stream.mkString(Stream.decodeText(child.stdout))], + { concurrency: "unbounded" }, + ); + expect(exitCode).toBe(0); + expect(output).toBe(`${longSetting}${expected}`); + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); + + it.live( + `preserves role setting bytes with keep-comments=${keepComments}, dry-run=${dryRun}`, + () => + Effect.gen(function* () { + const encoder = new TextEncoder(); + const setting = Uint8Array.from([ + ...encoder.encode('ALTER ROLE "app_user" SET "myapp.bytes" TO \'café'), + 0xff, + ...encoder.encode("';\n"), + ]); + const bytes = Uint8Array.from([...setting, ...encoder.encode(`${reservedGrant}\n`)]); + const expectedBytes = Uint8Array.from([ + ...setting, + ...encoder.encode(`${keepComments ? `-- ${reservedGrant}\n` : ""}RESET ALL;\n`), + ]); + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const script = dryRun ? expandScript(dumpRoleScript, env) : dumpRoleScript; + /* Keep sed byte-oriented so its existing locale behavior cannot mask awk's. */ + const child = yield* spawner.spawn( + ChildProcess.make( + "bash", + ["-c", `pg_dumpall() { cat; }\nsed() { LC_ALL=C command sed "$@"; }\n${script}`], + { + env: { + ...env, + LC_ALL: process.platform === "darwin" ? "en_US.UTF-8" : "C.UTF-8", + }, + stdin: Stream.make(bytes), + stderr: "inherit", + }, + ), + ); + const [exitCode, chunks] = yield* Effect.all( + [child.exitCode, Stream.runCollect(child.stdout)], + { concurrency: "unbounded" }, + ); + expect(exitCode).toBe(0); + expect(Uint8Array.from(chunks.flatMap((chunk) => [...chunk]))).toEqual(expectedBytes); + }).pipe(Effect.scoped, Effect.provide(BunServices.layer)), + ); + } + }); + } +}); diff --git a/apps/cli/src/command-internal/pg-dump.run.ts b/apps/cli/src/command-internal/pg-dump.run.ts index 8232be1e4a..4a412d32e2 100644 --- a/apps/cli/src/command-internal/pg-dump.run.ts +++ b/apps/cli/src/command-internal/pg-dump.run.ts @@ -232,6 +232,7 @@ const transformDumpLine = ( line: string, script: string, env: Readonly>, + inQuotedSql = false, ): string | undefined => { const applyExtraSed = (value: string | undefined): string | undefined => env["EXTRA_SED"] === "/^--/d" && value?.startsWith("--") ? undefined : value; @@ -249,6 +250,13 @@ const transformDumpLine = ( return dropComments(allowedConfig.test(line) ? line : `-- ${line}`); if (new RegExp(`GRANT ".*" TO "(${reservedRoles})"`, "u").test(line)) return dropComments(`-- ${line}`); + if ( + !inQuotedSql && + new RegExp(`^GRANT [A-Z ,]+ ON PARAMETER ".*" TO "(${reservedRoles})"([ ;]|$)`, "u").test( + line, + ) + ) + return dropComments(`-- ${line}`); if (line.startsWith("-- ") && allowedConfig.test(line.slice(3))) return dropComments(line.slice(3)); if (line.startsWith("-- ") && line.includes(" GRANT ")) return dropComments(line); @@ -304,6 +312,25 @@ const stackDumpStdout = ( const encoder = new TextEncoder(); let carry = ""; let previous: string | undefined; + let quote = ""; + const transformLine = (line: string) => { + const inQuotedSql = quote !== ""; + // pg_dumpall doubles quotes in literals and identifiers, including E-strings. + if (script.includes("pg_dumpall")) { + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const next = line[i + 1]; + if (quote !== "") { + if (char === quote) { + if (next === quote) i++; + else quote = ""; + } + } else if (char === "'" || char === '"') quote = char; + else if (char === "-" && next === "-") break; + } + } + return transformDumpLine(line, script, env, inQuotedSql); + }; const onChunk = (bytes: Uint8Array) => Effect.suspend(() => { carry += decoder.decode(bytes, { stream: true }); @@ -312,7 +339,7 @@ const stackDumpStdout = ( return Effect.forEach( lines, (line) => { - const transformed = transformDumpLine(line, script, env); + const transformed = transformLine(line); if ( transformed === undefined || (script.includes("pg_dumpall") && transformed === previous) @@ -327,7 +354,7 @@ const stackDumpStdout = ( const flush = () => Effect.suspend(() => { carry += decoder.decode(); - const final = transformDumpLine(carry, script, env); + const final = transformLine(carry); return final === undefined || final.length === 0 || (script.includes("pg_dumpall") && final === previous) diff --git a/apps/cli/src/command-internal/pg-dump.scripts.ts b/apps/cli/src/command-internal/pg-dump.scripts.ts index cbce14c3df..2dbb187405 100644 --- a/apps/cli/src/command-internal/pg-dump.scripts.ts +++ b/apps/cli/src/command-internal/pg-dump.scripts.ts @@ -8,4 +8,4 @@ export const dumpDataScript = '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Disable triggers so that data dump can be restored exactly as it is\necho "SET session_replication_role = replica;\n"\n\n# Explanation of pg_dump flags:\n#\n# --exclude-schema omit data from internal schemas as they are maintained by platform\n# --exclude-table omit data from migration history tables as they are managed by platform\n# --column-inserts only column insert syntax is supported, ie. no copy from stdin\n# --schema \'*\' include all other schemas by default\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n#\n# Never delete SQL comments because multiline records may begin with them.\npg_dump \\\n --data-only \\\n --quote-all-identifier \\\n --role "postgres" \\\n --exclude-schema "${EXCLUDED_SCHEMAS:-}" \\\n --exclude-table "auth.schema_migrations" \\\n --exclude-table "storage.migrations" \\\n --exclude-table "supabase_functions.migrations" \\\n --schema "$INCLUDED_SCHEMAS" \\\n ${EXTRA_FLAGS:-} \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\'\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; export const dumpRoleScript = - '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; + '#!/usr/bin/env bash\nset -euo pipefail\n\nexport PGHOST="$PGHOST"\nexport PGPORT="$PGPORT"\nexport PGUSER="$PGUSER"\nexport PGPASSWORD="$PGPASSWORD"\nexport PGDATABASE="$PGDATABASE"\n\n# Explanation of pg_dumpall flags:\n#\n# --roles-only only include create, alter, and grant role statements\n#\n# Explanation of sed substitutions:\n#\n# - do not emit psql meta commands\n# - do not create or alter reserved roles as they are blocked by supautils\n# - explicitly allow altering safe attributes, ie. statement_timeout, pgrst.*\n# - discard role attributes that require superuser, ie. nosuperuser, noreplication\n# - do not alter membership grants by supabase_admin role\n# - do not grant parameter privileges to reserved roles outside quoted SQL\npg_dumpall \\\n --roles-only \\\n --role "postgres" \\\n --quote-all-identifier \\\n --no-role-passwords \\\n --no-comments \\\n| LC_ALL=C awk -v reserved_roles="$RESERVED_ROLES" \'\n# Parenthesized field references survive the CLI dry-run expander.\nBEGIN { single_quote = sprintf("%c", 39) }\n{\n line = $(0)\n if (quote == "" && line ~ ("^GRANT [A-Z ,]+ ON PARAMETER \\".*\\" TO \\"(" reserved_roles ")\\"([ ;]|$)"))\n $(0) = "-- " line\n print\n count = length(line)\n end = 0\n for (i = 1; i <= count; i++) {\n if (i > end) {\n chunk = substr(line, i, 1025)\n start = i\n end = i + 1023\n }\n char = substr(chunk, i - start + 1, 1)\n next_char = substr(chunk, i - start + 2, 1)\n if (quote != "") {\n if (char == quote) {\n if (next_char == quote) i++\n else quote = ""\n }\n } else if (char == single_quote || char == "\\"") quote = char\n else if (char == "-" && next_char == "-") break\n }\n}\' \\\n| sed -E \'s/^\\\\(un)?restrict .*$/-- &/\' \\\n| sed -E "s/^CREATE ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/^ALTER ROLE \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "s/ (NOSUPERUSER|NOREPLICATION)//g" \\\n| sed -E "s/^-- (.* SET \\"($ALLOWED_CONFIGS)\\" .*)/\\1/" \\\n| sed -E "s/GRANT \\".*\\" TO \\"($RESERVED_ROLES)\\"/-- &/" \\\n| sed -E "${EXTRA_SED:-}" \\\n| uniq\n\n# Reset session config generated by pg_dump\necho "RESET ALL;"\n'; From 40238ecfe052f660ab02d3a0d86e0fafaad37bb2 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:59:42 +0000 Subject: [PATCH 16/33] refactor(cli): move the delegated binary and project link to effect (CLI-2837) (#7300) ## TL;DR the delegated binary lookup and the project link writes run through effect, ready for the effect lint ## whats improved/fixed? - the binary lookup checks its paths through `FileSystem` and reads its override env var through `Config` - `.link.json` is written through schema json instead of `JSON.stringify` - the lookup and the permissions help tests run as effects with typed errors ## how? - the lookup keeps the same search order and the same not found message - the link file keeps the same pretty printed bytes > [!NOTE] > nothing changes for users, the same binary is found and `.link.json` is exactly the same > these files join the effect lint in the last page of this stack. ## ref: - related: CLI-2837 --- .../help-section.integration.test.ts | 47 +++++-- .../help-section.tree.unit.test.ts | 104 ++++++++-------- .../go-proxy-target-env.integration.test.ts | 4 +- .../src/command-internal/go-proxy.layer.ts | 116 +++++++++--------- .../go-proxy.layer.unit.test.ts | 93 ++++++++------ apps/cli/src/command-internal/project-link.ts | 5 +- apps/cli/src/command-internal/viper-env.ts | 2 +- 7 files changed, 208 insertions(+), 163 deletions(-) diff --git a/apps/cli/src/command-internal/command-permissions/help-section.integration.test.ts b/apps/cli/src/command-internal/command-permissions/help-section.integration.test.ts index 74c989c158..35f2bf4638 100644 --- a/apps/cli/src/command-internal/command-permissions/help-section.integration.test.ts +++ b/apps/cli/src/command-internal/command-permissions/help-section.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { Console, Effect, Layer, Option } from "effect"; +import { Console, Effect, Layer, Option, Schema } from "effect"; import { CliOutput, Command } from "effect/cli"; import { emptyEnv, fakeConsole, mockOutput } from "../../../tests/helpers/mocks.ts"; @@ -17,6 +17,33 @@ import { YesFlag, } from "../global-flags.ts"; +/* The slice of a command's JSON help that the section assertions read. */ +const HelpJson = Schema.Struct({ + doc: Schema.Unknown, + permissions: Schema.Struct({ + schemes: Schema.Array( + Schema.Struct({ + groups: Schema.Array( + Schema.Struct({ + heading: Schema.String, + rows: Schema.Array( + Schema.Union([ + Schema.Struct({ anyOf: Schema.Array(Schema.Array(Schema.String)) }), + Schema.Struct({ + cases: Schema.Array( + Schema.Struct({ for: Schema.String, allOf: Schema.Array(Schema.String) }), + ), + }), + Schema.Struct({ note: Schema.String }), + ]), + ), + }), + ), + }), + ), + }), +}); + const layerFor = ( args: ReadonlyArray, console: Console.Console, @@ -77,9 +104,12 @@ describe("token permissions in command help", () => { Effect.gen(function* () { const text = yield* helpFor(["api-keys", "--help"]); const printed = yield* helpFor(["api-keys", "--help"], jsonCliOutputFormatter()); - const json = JSON.parse(printed.replace(/^log:/, "")); + const raw = yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))( + printed.replace(/^log:/, ""), + ); + const json = yield* Schema.decodeUnknownEffect(HelpJson)(raw); - expect(json.permissions).toEqual({ + expect(raw).toHaveProperty("permissions", { status: "mapped", schemes: [ { @@ -113,7 +143,9 @@ describe("token permissions in command help", () => { }, ], }); - expect(JSON.stringify(json.doc)).not.toContain("supabase/Command"); + expect( + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))(json.doc), + ).not.toContain("supabase/Command"); const block = text.slice(text.indexOf("PERMISSIONS")); for (const scheme of json.permissions.schemes) { @@ -122,12 +154,9 @@ describe("token permissions in command help", () => { for (const row of group.rows) { const shown: ReadonlyArray = "anyOf" in row - ? row.anyOf.map((alternative: ReadonlyArray) => alternative.join(" + ")) + ? row.anyOf.map((alternative) => alternative.join(" + ")) : "cases" in row - ? row.cases.map( - (item: { for: string; allOf: ReadonlyArray }) => - `${item.allOf.join(" + ")} for ${item.for}`, - ) + ? row.cases.map((item) => `${item.allOf.join(" + ")} for ${item.for}`) : [row.note]; for (const text of shown) expect(block).toContain(text); } diff --git a/apps/cli/src/command-internal/command-permissions/help-section.tree.unit.test.ts b/apps/cli/src/command-internal/command-permissions/help-section.tree.unit.test.ts index b48f858f26..a506950c1f 100644 --- a/apps/cli/src/command-internal/command-permissions/help-section.tree.unit.test.ts +++ b/apps/cli/src/command-internal/command-permissions/help-section.tree.unit.test.ts @@ -1,7 +1,7 @@ import { BunServices } from "@effect/platform-bun"; import { Context, Effect, Exit, Option } from "effect"; import type { Command } from "effect/cli"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "@effect/vitest"; import { commandTreeFor } from "../../../tests/helpers/command-flags.ts"; import type { PermissionVariant } from "../../../tests/helpers/permission-registry.ts"; @@ -69,39 +69,41 @@ describe("permission help section across the command tree", () => { ); }; - it("gives a flag a default only when parsing no arguments yields that value", async () => { - for (const [path, node] of leaves) { - const declared = Option.getOrUndefined(readPermissions(node.command)); - if (declared?.status !== "mapped") continue; - const params = paramsFor(path); - // Help rebuilds a parent group's flags without defaults, so only these carry one. - const known = new Map( - [...rootFlags, ...permissionFlagsOf(ownParams(node.command))].map((flag) => [ - flag.name, - flag, - ]), - ); - const tested = new Set( - declared.operations.flatMap((entry) => - (entry.when ?? []).flatMap((condition) => - condition.oneOf === undefined ? [] : [condition.flag], + it.effect("gives a flag a default only when parsing no arguments yields that value", () => + Effect.gen(function* () { + for (const [path, node] of leaves) { + const declared = Option.getOrUndefined(readPermissions(node.command)); + if (declared?.status !== "mapped") continue; + const params = paramsFor(path); + // Help rebuilds a parent group's flags without defaults, so only these carry one. + const known = new Map( + [...rootFlags, ...permissionFlagsOf(ownParams(node.command))].map((flag) => [ + flag.name, + flag, + ]), + ); + const tested = new Set( + declared.operations.flatMap((entry) => + (entry.when ?? []).flatMap((condition) => + condition.oneOf === undefined ? [] : [condition.flag], + ), ), - ), - ); - for (const flag of tested) { - const param = params.get(flag); - if (flag === undefined || param === undefined) continue; - const exit = await Effect.runPromiseExit( - param.parse({ flags: {}, arguments: [] }).pipe(Effect.provide(BunServices.layer)), ); - const parsed = Exit.isSuccess(exit) ? exit.value[1] : undefined; - expect( - known.get(flag)?.default, - `"${path}" --${flag} default differs from parsing no arguments`, - ).toBe(typeof parsed === "string" ? parsed : undefined); + for (const flag of tested) { + const param = params.get(flag); + if (flag === undefined || param === undefined) continue; + const exit = yield* param + .parse({ flags: {}, arguments: [] }) + .pipe(Effect.provide(BunServices.layer), Effect.exit); + const parsed = Exit.isSuccess(exit) ? exit.value[1] : undefined; + expect( + known.get(flag)?.default, + `"${path}" --${flag} default differs from parsing no arguments`, + ).toBe(typeof parsed === "string" ? parsed : undefined); + } } - } - }); + }), + ); it("never tests the presence of a text flag that has a default", () => { for (const [path, node] of leaves) { @@ -120,28 +122,30 @@ describe("permission help section across the command tree", () => { } }); - it("never turns on by default a switch a condition tests", async () => { - for (const [path, node] of leaves) { - const declared = Option.getOrUndefined(readPermissions(node.command)); - if (declared?.status !== "mapped") continue; - const params = paramsFor(path); - const tested = new Set( - declared.operations.flatMap((entry) => - (entry.when ?? []).flatMap((condition) => - condition.is === undefined ? [] : [condition.flag], + it.effect("never turns on by default a switch a condition tests", () => + Effect.gen(function* () { + for (const [path, node] of leaves) { + const declared = Option.getOrUndefined(readPermissions(node.command)); + if (declared?.status !== "mapped") continue; + const params = paramsFor(path); + const tested = new Set( + declared.operations.flatMap((entry) => + (entry.when ?? []).flatMap((condition) => + condition.is === undefined ? [] : [condition.flag], + ), ), - ), - ); - for (const flag of tested) { - const param = params.get(flag); - if (param === undefined || catalogFor(path).get(flag)?.hidden !== false) continue; - const [, value] = await Effect.runPromise( - param.parse({ flags: {}, arguments: [] }).pipe(Effect.provide(BunServices.layer)), ); - expect(value, `"${path}" --${flag} defaults to on`).not.toBe(true); + for (const flag of tested) { + const param = params.get(flag); + if (param === undefined || catalogFor(path).get(flag)?.hidden !== false) continue; + const [, value] = yield* param + .parse({ flags: {}, arguments: [] }) + .pipe(Effect.provide(BunServices.layer)); + expect(value, `"${path}" --${flag} defaults to on`).not.toBe(true); + } } - } - }); + }), + ); it("records every flag a command accepts when it declares its permissions", () => { for (const [path, node] of leaves) { diff --git a/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts b/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts index 91125ffe75..b01cc5afe8 100644 --- a/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts +++ b/apps/cli/src/command-internal/go-proxy-target-env.integration.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; import { BunServices } from "@effect/platform-bun"; -import { Effect, FileSystem, Layer, Option, Path } from "effect"; +import { Effect, FileSystem, Layer, Option, Path, Schema } from "effect"; import { useTempWorkdir, VALID_TOKEN } from "../../tests/helpers/command-mocks.ts"; import { isolatedHomeLayer } from "../../tests/helpers/command-mocks.ts"; @@ -79,7 +79,7 @@ const writeStackLink = Effect.fnUntraced(function* (workdir: string, poolerUrl?: yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); yield* fs.writeFileString( path.join(workdir, "supabase", ".link.json"), - JSON.stringify({ + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ project: { ref: LINKED_REF }, active_branch: { ref: LINKED_REF, diff --git a/apps/cli/src/command-internal/go-proxy.layer.ts b/apps/cli/src/command-internal/go-proxy.layer.ts index 2bcd265e60..d9cf7320b5 100644 --- a/apps/cli/src/command-internal/go-proxy.layer.ts +++ b/apps/cli/src/command-internal/go-proxy.layer.ts @@ -1,9 +1,7 @@ -import { existsSync } from "node:fs"; import { createRequire } from "node:module"; import os from "node:os"; -import path from "node:path"; import process from "node:process"; -import { Config, Effect, FileSystem, Layer, Option, Path, Stream } from "effect"; +import { Config, Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect"; import * as ChildProcess from "effect/process/ChildProcess"; import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"; import { resolveWorkdir } from "../config/command-settings.layer.ts"; @@ -15,8 +13,11 @@ import { GoProxyInvocation } from "./go-proxy-invocation.ts"; import { GoProxy } from "./go-proxy.service.ts"; import { readProjectLinkFile } from "./project-link.ts"; import { usesStackState } from "./stack-backend.ts"; +import { shellEnv } from "./viper-env.ts"; import { withProcessSpan } from "../shared/telemetry/spans.ts"; +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + const markDelegated = Effect.serviceOption(GoProxyInvocation).pipe( Effect.flatMap((invocation) => Option.isSome(invocation) ? invocation.value.markDelegated : Effect.void, @@ -43,12 +44,12 @@ const makeStackStateEnv = ( const { project } = link.value; return { SUPABASE_INTERNAL_STACK_STATE: "1", - SUPABASE_INTERNAL_LINKED_PROJECT: JSON.stringify({ + SUPABASE_INTERNAL_LINKED_PROJECT: yield* encodeJson({ ref: project.ref, name: project.name ?? "", organization_id: project.organization_id ?? "", organization_slug: project.organization_slug ?? "", - }), + }).pipe(Effect.orDie), }; }); @@ -73,10 +74,15 @@ export type BinaryResolution = | { readonly found: string } | { readonly notFound: ReadonlyArray }; -function resolveBinary(): BinaryResolution { +const resolveBinary = Effect.fnUntraced(function* ( + fs: FileSystem.FileSystem, + path: Path.Path, +): Effect.fn.Return { + const exists = (candidate: string) => + fs.exists(candidate).pipe(Effect.orElseSucceed(() => false)); const tried: string[] = []; - const envBin = process.env["SUPABASE_GO_BINARY"]; + const envBin = yield* shellEnv("SUPABASE_GO_BINARY"); if (envBin) return { found: envBin }; tried.push("$SUPABASE_GO_BINARY (unset)"); @@ -85,25 +91,27 @@ function resolveBinary(): BinaryResolution { // When running as a compiled standalone binary (exec'd by the base shim), process.execPath is // this binary's own path; look for supabase-go co-located next to it. const colocated = path.join(path.dirname(process.execPath), `supabase-go${ext}`); - if (existsSync(colocated)) return { found: colocated }; + if (yield* exists(colocated)) return { found: colocated }; tried.push(`${colocated} (not found alongside the shim)`); // When running from source, resolve via installed npm packages. - // Guard with existsSync — in dev the workspace stub packages exist but their bin/ is empty. + // Guard with exists — in dev the workspace stub packages exist but their bin/ is empty. const candidates = PLATFORM_CANDIDATES[process.platform]?.[os.arch()] ?? []; for (const suffix of candidates) { - try { - const pkgPath = path.dirname(require.resolve(`@supabase/cli-${suffix}/package.json`)); - const bin = path.join(pkgPath, "bin", `supabase-go${ext}`); - if (existsSync(bin)) return { found: bin }; - tried.push(`${bin} (npm package present, binary missing)`); - } catch { + const pkgJson = yield* Effect.try(() => + require.resolve(`@supabase/cli-${suffix}/package.json`), + ).pipe(Effect.option); + if (Option.isNone(pkgJson)) { tried.push(`@supabase/cli-${suffix} (npm package not installed)`); + continue; } + const bin = path.join(path.dirname(pkgJson.value), "bin", `supabase-go${ext}`); + if (yield* exists(bin)) return { found: bin }; + tried.push(`${bin} (npm package present, binary missing)`); } return { notFound: tried }; -} +}); /** * Builds a `curl | tar` install snippet for the host platform, using the version baked into this @@ -177,7 +185,7 @@ export function makeGoProxyLayer(opts?: { parentOwnsCapturedSuccessTail?: boolean; /** * Override binary resolution. Primarily a test seam so specs don't have to - * mutate `process.env.SUPABASE_GO_BINARY` or stub the filesystem: + * pin `SUPABASE_GO_BINARY` or stub the filesystem: * - `string` — treat as the resolved Go binary path. * - `{ notFound: [...] }` — simulate the not-found path; `.exec` will print * the diagnostic and fail with a non-zero exit code. @@ -202,7 +210,7 @@ export function makeGoProxyLayer(opts?: { const resolved: BinaryResolution = typeof opts?.binary === "string" ? { found: opts.binary } - : (opts?.binary ?? resolveBinary()); + : (opts?.binary ?? (yield* resolveBinary(fs, path))); const globalArgs = opts?.globalArgs ?? []; const projectWorkdir = Effect.gen(function* () { const envValue = yield* Config.option(Config.String("SUPABASE_WORKDIR")).pipe( @@ -227,12 +235,10 @@ export function makeGoProxyLayer(opts?: { yield* Effect.sync(() => { process.stderr.write(`${formatGoBinaryNotFoundError(resolved.notFound)}\n`); }); - return yield* Effect.fail( - new GoChildExitError({ - exitCode: 1, - message: "supabase-go binary not found", - }), - ); + return yield* new GoChildExitError({ + exitCode: 1, + message: "supabase-go binary not found", + }); } const binary = resolved.found; @@ -273,12 +279,10 @@ export function makeGoProxyLayer(opts?: { .pipe(Effect.orDie), ); if (exitCode !== 0) { - return yield* Effect.fail( - new GoChildExitError({ - exitCode, - message: `supabase-go exited with code ${exitCode} (see stderr for details)`, - }), - ); + return yield* new GoChildExitError({ + exitCode, + message: `supabase-go exited with code ${exitCode} (see stderr for details)`, + }); } yield* markDelegated; }), @@ -290,12 +294,10 @@ export function makeGoProxyLayer(opts?: { yield* Effect.sync(() => { process.stderr.write(`${formatGoBinaryNotFoundError(resolved.notFound)}\n`); }); - return yield* Effect.fail( - new GoChildExitError({ - exitCode: 1, - message: "supabase-go binary not found", - }), - ); + return yield* new GoChildExitError({ + exitCode: 1, + message: "supabase-go binary not found", + }); } const binary = resolved.found; yield* processControl.holdSignals(["SIGINT", "SIGTERM", "SIGHUP"]); @@ -322,36 +324,30 @@ export function makeGoProxyLayer(opts?: { { executable: binary, argCount: argv.length }, (traceEnv) => Effect.gen(function* () { - const handle = yield* spawner - .spawn( - ChildProcess.make(binary, argv, { - cwd: execOpts?.cwd ?? opts?.cwd, - env: { ...env, ...traceEnv }, - extendEnv: true, - stdin: execOpts?.stdin ?? "inherit", - stdout: "pipe", - stderr: "inherit", - detached: false, - }), - ) - .pipe(Effect.orDie); + const handle = yield* spawner.spawn( + ChildProcess.make(binary, argv, { + cwd: execOpts?.cwd ?? opts?.cwd, + env: { ...env, ...traceEnv }, + extendEnv: true, + stdin: execOpts?.stdin ?? "inherit", + stdout: "pipe", + stderr: "inherit", + detached: false, + }), + ); // Drain stdout fully before awaiting exit so a full pipe buffer can't // deadlock the child. - const captured = yield* Stream.mkString(Stream.decodeText(handle.stdout)).pipe( - Effect.orDie, - ); - const exitCode = yield* handle.exitCode.pipe(Effect.orDie); + const captured = yield* Stream.mkString(Stream.decodeText(handle.stdout)); + const exitCode = yield* handle.exitCode; return { captured, exitCode }; - }), + }).pipe(Effect.orDie), (result) => result.exitCode, ); if (exitCode !== 0) { - return yield* Effect.fail( - new GoChildExitError({ - exitCode, - message: `supabase-go exited with code ${exitCode} (see stderr for details)`, - }), - ); + return yield* new GoChildExitError({ + exitCode, + message: `supabase-go exited with code ${exitCode} (see stderr for details)`, + }); } if (opts?.parentOwnsCapturedSuccessTail !== true) { yield* markDelegated; diff --git a/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts b/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts index 61bfd1cdad..a4bcfce155 100644 --- a/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts +++ b/apps/cli/src/command-internal/go-proxy.layer.unit.test.ts @@ -9,6 +9,8 @@ import { FileSystem, Layer, Path, + PlatformError, + Schema, Sink, Stream, } from "effect"; @@ -100,7 +102,7 @@ function mockProcessControl() { ).pipe(Effect.asVoid), exit, setExitCode: () => Effect.void, - getExitCode: Effect.succeed(undefined), + getExitCode: Effect.undefined, }), ), }; @@ -131,7 +133,14 @@ function mockSpawner(exit: ExitBehavior, spawnedBeforeExit?: Deferred.Deferred { const TRIED = ["$SUPABASE_GO_BINARY (unset)"]; const PINNED_VERSION = "2.100.0"; - async function withMockedHost( + function withMockedHost( opts: { platform: NodeJS.Platform; arch: NodeJS.Architecture }, - fn: (mod: typeof import("./go-proxy.layer.ts")) => void | Promise, - ): Promise { - vi.resetModules(); - vi.doMock("../shared/cli/version.ts", () => ({ CLI_VERSION: PINNED_VERSION })); - const originalPlatform = process.platform; - const originalArch = process.arch; - Object.defineProperty(process, "platform", { value: opts.platform, configurable: true }); - Object.defineProperty(process, "arch", { value: opts.arch, configurable: true }); - try { - const mod = await import("./go-proxy.layer.ts"); - await fn(mod); - } finally { - Object.defineProperty(process, "platform", { - value: originalPlatform, - configurable: true, - }); - Object.defineProperty(process, "arch", { value: originalArch, configurable: true }); - vi.doUnmock("../cli/version.ts"); - vi.resetModules(); - } + fn: (mod: typeof import("./go-proxy.layer.ts")) => void, + ): Effect.Effect { + return Effect.acquireUseRelease( + Effect.sync(() => { + vi.resetModules(); + vi.doMock("../shared/cli/version.ts", () => ({ CLI_VERSION: PINNED_VERSION })); + const originalPlatform = process.platform; + const originalArch = process.arch; + Object.defineProperty(process, "platform", { value: opts.platform, configurable: true }); + Object.defineProperty(process, "arch", { value: opts.arch, configurable: true }); + return { originalPlatform, originalArch }; + }), + () => Effect.promise(() => import("./go-proxy.layer.ts")).pipe(Effect.map(fn)), + ({ originalPlatform, originalArch }) => + Effect.sync(() => { + Object.defineProperty(process, "platform", { + value: originalPlatform, + configurable: true, + }); + Object.defineProperty(process, "arch", { value: originalArch, configurable: true }); + vi.doUnmock("../cli/version.ts"); + vi.resetModules(); + }), + ); } - it("renders a copy-pasteable install snippet for linux x64", async () => { - await withMockedHost({ platform: "linux", arch: "x64" }, (mod) => { + it.effect("renders a copy-pasteable install snippet for linux x64", () => + withMockedHost({ platform: "linux", arch: "x64" }, (mod) => { const message = mod.formatGoBinaryNotFoundError(TRIED); expect(message).toContain( `https://github.com/supabase/cli/releases/download/v${PINNED_VERSION}/supabase_${PINNED_VERSION}_linux_amd64.tar.gz`, ); expect(message).toContain(`mkdir -p "$HOME/.local/share/supabase"`); expect(message).toContain(`export PATH="$HOME/.local/share/supabase:$PATH"`); - }); - }); + }), + ); - it("maps Node's win32 platform to the release asset's `windows` slug", async () => { - await withMockedHost({ platform: "win32", arch: "x64" }, (mod) => { + it.effect("maps Node's win32 platform to the release asset's `windows` slug", () => + withMockedHost({ platform: "win32", arch: "x64" }, (mod) => { const message = mod.formatGoBinaryNotFoundError(TRIED); expect(message).toContain( `https://github.com/supabase/cli/releases/download/v${PINNED_VERSION}/supabase_${PINNED_VERSION}_windows_amd64.tar.gz`, ); expect(message).not.toContain("win32"); - }); - }); + }), + ); - it("maps darwin arm64 to the matching release asset", async () => { - await withMockedHost({ platform: "darwin", arch: "arm64" }, (mod) => { + it.effect("maps darwin arm64 to the matching release asset", () => + withMockedHost({ platform: "darwin", arch: "arm64" }, (mod) => { expect(mod.formatGoBinaryNotFoundError(TRIED)).toContain( `supabase_${PINNED_VERSION}_darwin_arm64.tar.gz`, ); - }); - }); + }), + ); - it("omits the snippet on unsupported architectures (no release asset)", async () => { - await withMockedHost({ platform: "linux", arch: "ia32" }, (mod) => { + it.effect("omits the snippet on unsupported architectures (no release asset)", () => + withMockedHost({ platform: "linux", arch: "ia32" }, (mod) => { expect(mod.formatGoBinaryNotFoundError(TRIED)).not.toContain("curl -sL"); - }); - }); + }), + ); }); const proxyDeps = ( @@ -408,7 +421,7 @@ describe("makeGoProxyLayer", () => { ); const linked = { stackState: "1", - linked: JSON.stringify({ + linked: yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ ref: "abcdefghijklmnopqrst", name: "Acme", organization_id: "org_123", diff --git a/apps/cli/src/command-internal/project-link.ts b/apps/cli/src/command-internal/project-link.ts index 77fb833b83..3817e92bf3 100644 --- a/apps/cli/src/command-internal/project-link.ts +++ b/apps/cli/src/command-internal/project-link.ts @@ -9,6 +9,8 @@ import { ErrorActionabilityId, } from "../shared/telemetry/error-actionability.ts"; +const encodePrettyJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 })); + /** Document name under `/supabase`, local to one checkout. */ const PROJECT_LINK_FILENAME = ".link.json"; @@ -167,7 +169,8 @@ export const writeProjectLink = Effect.fn("ProjectLink.write")(function* ( yield* Effect.gen(function* () { yield* fs.makeDirectory(path.dirname(file), { recursive: true }); yield* ensureGitignore(fs, path, workdir); - yield* publish(fs, path, { target: file, content: `${JSON.stringify(link, null, 2)}\n` }); + const content = yield* encodePrettyJson(link).pipe(Effect.orDie); + yield* publish(fs, path, { target: file, content: `${content}\n` }); }).pipe( Effect.catchTags({ PlatformError: (error) => diff --git a/apps/cli/src/command-internal/viper-env.ts b/apps/cli/src/command-internal/viper-env.ts index 47c095ccc2..96588f2866 100644 --- a/apps/cli/src/command-internal/viper-env.ts +++ b/apps/cli/src/command-internal/viper-env.ts @@ -16,7 +16,7 @@ function viperBool(raw: string | undefined): boolean { } /** Env-record providers never fail this read, so a `ConfigError` here is a defect. */ -const shellEnv = (name: string) => +export const shellEnv = (name: string) => Config.option(Config.String(name)).pipe(Effect.map(Option.getOrUndefined), Effect.orDie); /** Reads a single `SUPABASE_*` boolean env var through `Config` (see module doc). */ From 58c359602466d2cfaf89f489e3fbbe3817014f51 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:59:43 +0000 Subject: [PATCH 17/33] refactor(cli): resolve local config values through effect (CLI-2837) (#7301) ## TL;DR local config values, config validation and path containment run through effect, so the effect lint now covers `apps/cli/src` ## whats improved/fixed? - they read env through `Config` and files through `FileSystem`, and fail with typed errors instead of throwing - the config value tests pin env through a config provider instead of editing `process.env`, so a shell override can no longer skew them - the effect lint allow list swaps its per-file `command-internal` entries for one `!apps/cli/src/**` entry ## how? - `start`, `status`, `stop`, `services`, `notebooks`, `config push` and `stack start` yield the converted helpers and map failures with the same messages they used to catch - the error classes keep their names, so error codes and telemetry fingerprints match develop. - `SIDE_EFFECTS.md` notes now say `fs.realPath` where they said `realpathSync` > [!NOTE] > (same here) nothing changes for users, > every command prints the same output and exits with the same codes and errors. > > `local-db-running` stays excluded, it talks to the docker socket or named pipe over node http and needs a windows check first. ## ref: - closes: CLI-2837 --- .oxlintrc.effect.json | 122 +- ...ig-validate.deep-missing-path.unit.test.ts | 61 +- .../config-validate.parity.unit.test.ts | 84 +- .../src/command-internal/config-validate.ts | 101 +- .../config-validate.unit.test.ts | 594 ++- .../db-bootstrap/bootstrap-config.ts | 136 +- .../db-bootstrap/local-container-inputs.ts | 30 +- .../db-bootstrap/start-local-database.ts | 284 +- .../command-internal/db-config.toml-read.ts | 70 +- .../command-internal/functions-go-config.ts | 36 +- .../command-internal/local-config-values.ts | 1640 ++++--- .../local-config-values.unit.test.ts | 4052 +++++++++-------- .../command-internal/local-project-context.ts | 4 +- .../src/command-internal/path-containment.ts | 127 +- apps/cli/src/command-internal/stack-config.ts | 469 +- .../cli/src/command-internal/status-values.ts | 57 +- .../status-values.unit.test.ts | 534 +-- .../command-internal/storage-credentials.ts | 87 +- .../src/commands/config/push/SIDE_EFFECTS.md | 2 +- .../config/push/push.auth-email-content.ts | 19 +- apps/cli/src/commands/db/diff/SIDE_EFFECTS.md | 2 +- .../experimental/stack/start/start.handler.ts | 76 +- .../commands/functions/deploy/SIDE_EFFECTS.md | 2 +- .../functions/download/SIDE_EFFECTS.md | 2 +- .../commands/functions/serve/SIDE_EFFECTS.md | 2 +- .../commands/migration/squash/SIDE_EFFECTS.md | 2 +- .../commands/notebooks/notebooks.shared.ts | 23 +- .../commands/services/services-local-stack.ts | 65 +- apps/cli/src/commands/start/SIDE_EFFECTS.md | 4 +- apps/cli/src/commands/start/start.gates.ts | 28 +- apps/cli/src/commands/start/start.handler.ts | 339 +- .../start/start.services.unit.test.ts | 16 +- apps/cli/src/commands/status/SIDE_EFFECTS.md | 2 +- .../cli/src/commands/status/status.handler.ts | 30 +- apps/cli/src/commands/stop/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/stop/stop.handler.ts | 30 +- 36 files changed, 4884 insertions(+), 4250 deletions(-) diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index edf2b3ef1c..ad8cb1feb6 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -8,134 +8,20 @@ "**", "!packages/config/**", "!packages/stack/**", - "!apps/cli/src/auth/**", - "!apps/cli/src/cli/**", - "!apps/cli/src/commands/**", - "!apps/cli/src/config/**", - "!apps/cli/src/docs/**", - "!apps/cli/src/globals.d.ts", - "!apps/cli/src/main.ts", - "!apps/cli/src/output/**", - "!apps/cli/src/shared/**", - "!apps/cli/src/telemetry/**", + "!apps/cli/src/**", // Last match wins across the whole list: keep bare re-exclusions after every // `!` entry that would otherwise re-include them. "apps/cli/src/shared/cli/bin.ts", "apps/cli/src/shared/compute/stacks/**", "apps/cli/src/shared/feedback/database.types.ts", + "apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts", + "apps/cli/src/command-internal/db-bootstrap/local-db-running.ts", "!apps/cli/tests/helpers/branches-live.ts", "!apps/cli/tests/helpers/compute.ts", "!apps/cli/tests/helpers/migration-live.ts", "!apps/cli/tests/helpers/notebooks.ts", "!apps/cli/tests/helpers/postgres-config-live.ts", "!apps/cli/tests/helpers/secrets-live.ts", - "!apps/cli/tests/helpers/storage-live.ts", - "!apps/cli/src/command-internal/agent-mode.*", - "!apps/cli/src/command-internal/api-*", - "!apps/cli/src/command-internal/bitbucket-pipeline.ts", - "!apps/cli/src/command-internal/branch-ref.*", - "!apps/cli/src/command-internal/branch-target.*", - "!apps/cli/src/command-internal/bundled-postgres-client.*", - "!apps/cli/src/command-internal/colors.*", - "!apps/cli/src/command-internal/config-pull-run.*", - "!apps/cli/src/command-internal/config-validate.*", - "!apps/cli/src/command-internal/connect-errors.*", - "!apps/cli/src/command-internal/container-cli.*", - "!apps/cli/src/command-internal/db-*.ts", - "!apps/cli/src/command-internal/db-bootstrap/**", - "!apps/cli/src/command-internal/debug-logger.*", - "!apps/cli/src/command-internal/detect-content-type.*", - "!apps/cli/src/command-internal/diff-engine.*", - "!apps/cli/src/command-internal/docker-*.ts", - "!apps/cli/src/command-internal/dotenv.*", - "!apps/cli/src/command-internal/drop-objects.ts", - "!apps/cli/src/command-internal/edge-runtime-*.ts", - "!apps/cli/src/command-internal/ensure-login.*", - "!apps/cli/src/command-internal/error-message.ts", - "!apps/cli/src/command-internal/experimental-feature.ts", - "!apps/cli/src/command-internal/experimental-gate.*", - "!apps/cli/src/command-internal/fail-on.*", - "!apps/cli/src/command-internal/get-api-keys.*", - "!apps/cli/src/command-internal/get-tenant-api-keys.*", - "!apps/cli/src/command-internal/git-status.*", - "!apps/cli/src/command-internal/glob.*", - "!apps/cli/src/command-internal/global-flags.*", - "!apps/cli/src/command-internal/go-duration.*", - "!apps/cli/src/command-internal/go-float.*", - "!apps/cli/src/command-internal/go-json.*", - "!apps/cli/src/command-internal/go-jwt.*", - "!apps/cli/src/command-internal/go-output-flag.*", - "!apps/cli/src/command-internal/go-output.encoders.*", - "!apps/cli/src/command-internal/go-quote.ts", - "!apps/cli/src/command-internal/go-struct-output.*", - "!apps/cli/src/command-internal/hostname.*", - "!apps/cli/src/command-internal/http-*", - "!apps/cli/src/command-internal/identity-stitch.*", - "!apps/cli/src/command-internal/kong-*", - "!apps/cli/src/command-internal/link-services-core.*", - "!apps/cli/src/command-internal/linked-state.*", - "!apps/cli/src/command-internal/local-project-context.*", - "!apps/cli/src/command-internal/login-*", - "!apps/cli/src/command-internal/make-dir.*", - "!apps/cli/src/command-internal/management-api-runtime.*", - "!apps/cli/src/command-internal/migrate-and-seed.*", - "!apps/cli/src/command-internal/migration*", - "!apps/cli/src/command-internal/param-introspection.*", - "!apps/cli/src/command-internal/parent-project-ref.*", - "!apps/cli/src/command-internal/parse-uint.*", - "!apps/cli/src/command-internal/password-requirements.ts", - "!apps/cli/src/command-internal/path-match.*", - "!apps/cli/src/command-internal/pflag-reconcile.*", - "!apps/cli/src/command-internal/pg-dump.*", - "!apps/cli/src/command-internal/pg-net-guidance.*", - "!apps/cli/src/command-internal/pgdelta*", - "!apps/cli/src/command-internal/pgpass.*", - "!apps/cli/src/command-internal/pgservicefile.*", - "!apps/cli/src/command-internal/pooler-fallback.*", - "!apps/cli/src/command-internal/postgres-client.run.*", - "!apps/cli/src/command-internal/postgres-url.*", - "!apps/cli/src/command-internal/profile-load.*", - "!apps/cli/src/command-internal/profile.*", - "!apps/cli/src/command-internal/project-create-core.*", - "!apps/cli/src/command-internal/project-environment.*", - "!apps/cli/src/command-internal/project-target.*", - "!apps/cli/src/command-internal/prompt-yes-no.*", - "!apps/cli/src/command-internal/raw-http.*", - "!apps/cli/src/command-internal/ref-patterns.*", - "!apps/cli/src/command-internal/region.format.*", - "!apps/cli/src/command-internal/resolve-token.*", - "!apps/cli/src/command-internal/rune-width.*", - "!apps/cli/src/command-internal/schema-flags.*", - "!apps/cli/src/command-internal/seed*", - "!apps/cli/src/command-internal/service-catalog.*", - "!apps/cli/src/command-internal/service-version-overrides.*", - "!apps/cli/src/command-internal/shell-quote.ts", - "!apps/cli/src/command-internal/size-units.ts", - "!apps/cli/src/command-internal/sql-files-glob.*", - "!apps/cli/src/command-internal/sql-split.*", - "!apps/cli/src/command-internal/stack-*", - "!apps/cli/src/command-internal/start-*", - "!apps/cli/src/command-internal/status-errors.ts", - "!apps/cli/src/command-internal/status-pretty.*", - "!apps/cli/src/command-internal/status-values.*", - "!apps/cli/src/command-internal/storage-*.ts", - "!apps/cli/src/command-internal/string-slice-flag.*", - "!apps/cli/src/command-internal/temp-paths.*", - "!apps/cli/src/command-internal/tenant-*", - "!apps/cli/src/command-internal/test-db.*", - "!apps/cli/src/command-internal/timestamp.format.*", - "!apps/cli/src/command-internal/upgrade-notice.*", - "!apps/cli/src/command-internal/upgrade-suggest.*", - "!apps/cli/src/command-internal/vault-decrypt.*", - "!apps/cli/src/command-internal/vault.*", - "!apps/cli/src/command-internal/viper-env.*", - "!apps/cli/src/command-internal/workdir-project.*", - "!apps/cli/src/command-internal/workdir-search.ts", - "!apps/cli/src/command-internal/workdir-validation.ts", - "apps/cli/src/command-internal/config-validate.parity.unit.test.ts", - "apps/cli/src/command-internal/config-validate.ts", - "apps/cli/src/command-internal/config-validate.unit.test.ts", - "apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts", - "apps/cli/src/command-internal/db-bootstrap/local-db-running.ts" + "!apps/cli/tests/helpers/storage-live.ts" ] } diff --git a/apps/cli/src/command-internal/config-validate.deep-missing-path.unit.test.ts b/apps/cli/src/command-internal/config-validate.deep-missing-path.unit.test.ts index d619b6b5e3..4c72b7b8d2 100644 --- a/apps/cli/src/command-internal/config-validate.deep-missing-path.unit.test.ts +++ b/apps/cli/src/command-internal/config-validate.deep-missing-path.unit.test.ts @@ -1,47 +1,54 @@ import * as BunPath from "@effect/platform-bun/BunPath"; -import { Effect, Path } from "effect"; -import { describe, expect, it, vi } from "vitest"; +import { Effect, FileSystem, Layer, Path, PlatformError } from "effect"; +import { describe, expect, it } from "vitest"; + +import { resolveEmailTemplateContentPath } from "./config-validate.ts"; /** * Exercises `canonicalPathForContainment`'s ancestor walk-up with a 5,000-component missing * path — deeper than any real filesystem allows (`PATH_MAX` caps a real OS at a few thousand - * components), so `node:fs` is mocked here to drive the loop's own iteration count directly. - * Isolated into its own file because `vi.mock` is file-scoped and would break - * `config-validate.unit.test.ts`'s reliance on a real filesystem. + * components), so `FileSystem` is faked here to drive the loop's own iteration count directly. */ -vi.mock("node:fs", () => ({ - realpathSync: vi.fn((path: string) => { - if (path === FAKE_EXISTING_BASE) return FAKE_EXISTING_BASE; - throw Object.assign(new Error(`ENOENT: no such file or directory, realpath '${path}'`), { +const missingEntry = (method: string, syscall: string, path: string) => + PlatformError.systemError({ + _tag: "NotFound", + module: "FileSystem", + method, + syscall, + pathOrDescriptor: path, + cause: Object.assign(new Error(`ENOENT: no such file or directory, ${syscall} '${path}'`), { code: "ENOENT", - }); - }), - lstatSync: vi.fn(() => undefined), - readlinkSync: vi.fn(() => { - throw new Error("readlinkSync should never be reached — no path in this fixture is a symlink"); - }), - statSync: vi.fn(() => { - throw new Error("statSync should never be reached by the template content_path branch"); - }), -})); + }), + }); const FAKE_EXISTING_BASE = "/fake/project-root"; const path = Effect.runSync(Effect.provide(Path.Path, BunPath.layer)); +const fakeFileSystem = FileSystem.layerNoop({ + realPath: (path) => + path === FAKE_EXISTING_BASE + ? Effect.succeed(FAKE_EXISTING_BASE) + : Effect.fail(missingEntry("realPath", "realpath", path)), + /* The `lstat` stand-in: no path in this fixture exists, so none is a symlink. */ + readLink: (path) => Effect.fail(missingEntry("readLink", "readlink", path)), + stat: () => + Effect.die(new Error("`stat` should never be reached by the template content_path branch")), +}); describe("canonicalPathForContainment (via resolveEmailTemplateContentPath)", () => { - it("resolves a 5,000-component-deep missing content_path without blowing the call stack", () => - import("./config-validate.ts").then(({ resolveEmailTemplateContentPath }) => { - const missingSegments = Array.from({ length: 5000 }, (_, i) => `missing-${i}`); - const contentPath = `${missingSegments.join("/")}/invite.html`; + it("resolves a 5,000-component-deep missing content_path without blowing the call stack", () => { + const missingSegments = Array.from({ length: 5000 }, (_, i) => `missing-${i}`); + const contentPath = `${missingSegments.join("/")}/invite.html`; - const resolved = resolveEmailTemplateContentPath({ + const resolved = Effect.runSync( + resolveEmailTemplateContentPath({ section: "template", name: "invite", contentPath, contentPresent: false, base: FAKE_EXISTING_BASE, - }); + }).pipe(Effect.provide(Layer.mergeAll(fakeFileSystem, BunPath.layer))), + ); - expect(resolved).toBe(path.join(FAKE_EXISTING_BASE, contentPath)); - })); + expect(resolved).toBe(path.join(FAKE_EXISTING_BASE, contentPath)); + }); }); diff --git a/apps/cli/src/command-internal/config-validate.parity.unit.test.ts b/apps/cli/src/command-internal/config-validate.parity.unit.test.ts index 17b8f86b53..78c39b877f 100644 --- a/apps/cli/src/command-internal/config-validate.parity.unit.test.ts +++ b/apps/cli/src/command-internal/config-validate.parity.unit.test.ts @@ -1,13 +1,10 @@ import { unusedStackServices } from "../../tests/helpers/unused-stack.ts"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; import { CliConfigSchema, type CliConfig } from "@supabase/config"; import { Effect, Exit, FileSystem, Layer, Path, Schema } from "effect"; -import { withEnvVar, mockCommandSettings } from "../../tests/helpers/command-mocks.ts"; +import { withConfigEnv, mockCommandSettings } from "../../tests/helpers/command-mocks.ts"; import { CommandPlatformApiFactory } from "../auth/command-platform-api-factory.service.ts"; import { readDbToml } from "./db-config.toml-read.ts"; import { resolveStorageCredentials } from "./storage-credentials.ts"; @@ -28,12 +25,21 @@ import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts"; * the `baseConfig`/`WORKDIR` pattern from `local-config-values.unit.test.ts`. */ -function withConfig(content: string) { - const dir = mkdtempSync(join(tmpdir(), "config-validate-parity-")); - mkdirSync(join(dir, "supabase"), { recursive: true }); - writeFileSync(join(dir, "supabase", "config.toml"), content); +const withConfig = Effect.fnUntraced(function* (content: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectory({ prefix: "config-validate-parity-" }); + yield* fs.makeDirectory(path.join(dir, "supabase"), { recursive: true }); + yield* fs.writeFileString(path.join(dir, "supabase", "config.toml"), content); return dir; -} +}); + +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); +const removeDir = (dir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(dir, { recursive: true, force: true }); + }); const readD = (workdir: string) => Effect.gen(function* () { @@ -45,14 +51,14 @@ const readD = (workdir: string) => /** Drives D's real pipeline and asserts the failure message contains `message`. */ function failsWithD(tomlLines: ReadonlyArray, message: string) { return Effect.gen(function* () { - const dir = withConfig(tomlLines.join("\n")); + const dir = yield* withConfig(tomlLines.join("\n")); const exit = yield* readD(dir).pipe(Effect.exit); expect(Exit.isFailure(exit), `D: expected failure containing: ${message}`).toBe(true); if (Exit.isFailure(exit)) { - expect(JSON.stringify(exit.cause)).toContain(message); + expect(yield* encodeJson(exit.cause)).toContain(message); } - rmSync(dir, { recursive: true, force: true }); - }); + yield* removeDir(dir); + }).pipe(Effect.provide(BunServices.layer)); } const decodeConfig = Schema.decodeUnknownSync(CliConfigSchema); @@ -63,16 +69,19 @@ function baseConfig(overrides: Record = {}): CliConfig { } /** Drives L's real pipeline and asserts the failure message contains `message`. */ -function failsWithL( +const failsWithL = Effect.fnUntraced(function* ( overrides: Record, message: string, document?: Readonly>, ) { const config = baseConfig(overrides); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)).toThrow( - message, + const error = yield* Effect.flip( + resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document).pipe( + Effect.provide(BunServices.layer), + ), ); -} + expect(error.message).toContain(message); +}); interface ParityScenario { readonly name: string; @@ -303,7 +312,7 @@ describe("validateResolvedConfig cross-caller parity (D vs L)", () => { it.effect(`${scenario.name}: D and L fail with the same message`, () => Effect.gen(function* () { yield* failsWithD(scenario.toml, scenario.message); - failsWithL(scenario.overrides, scenario.message, scenario.document); + yield* failsWithL(scenario.overrides, scenario.message, scenario.document); }), ); } @@ -317,7 +326,7 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", /** Drives S's real pipeline (`resolveStorageCredentials`, local branch) to failure. */ const failsWithS = (config: CliConfig, message: string) => Effect.gen(function* () { - const dir = withConfig(""); + const dir = yield* withConfig(""); const exit = yield* resolveStorageCredentials({ projectRef: "", config }).pipe( Effect.provide( Layer.mergeAll( @@ -336,30 +345,35 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", ); expect(Exit.isFailure(exit), `S: expected failure containing: ${message}`).toBe(true); if (Exit.isFailure(exit)) { - expect(JSON.stringify(exit.cause)).toContain(message); + expect(yield* encodeJson(exit.cause)).toContain(message); } - rmSync(dir, { recursive: true, force: true }); - }); + yield* removeDir(dir); + }).pipe(Effect.provide(BunServices.layer)); // Ambient SUPABASE_API_* / SUPABASE_AUTH_* values would override the config // under test, so pin every participating key to unset for the duration of // each scenario. const isolated = (body: Effect.Effect) => - [ - "SUPABASE_API_PORT", - "SUPABASE_API_ENABLED", - "SUPABASE_API_TLS_ENABLED", - "SUPABASE_API_TLS_CERT_PATH", - "SUPABASE_API_TLS_KEY_PATH", - "SUPABASE_AUTH_JWT_SECRET", - "SUPABASE_AUTH_SERVICE_ROLE_KEY", - ].reduce((inner, name) => withEnvVar(name, undefined, inner), body); + withConfigEnv( + Object.fromEntries( + [ + "SUPABASE_API_PORT", + "SUPABASE_API_ENABLED", + "SUPABASE_API_TLS_ENABLED", + "SUPABASE_API_TLS_CERT_PATH", + "SUPABASE_API_TLS_KEY_PATH", + "SUPABASE_AUTH_JWT_SECRET", + "SUPABASE_AUTH_SERVICE_ROLE_KEY", + ].map((name) => [name, ""]), + ), + body, + ); it.effect("api.port = 0 with the API enabled: S and L fail with the same message", () => isolated( Effect.gen(function* () { const message = "Missing required field in config: api.port"; - failsWithL({ api: { port: 0 } }, message); + yield* failsWithL({ api: { port: 0 } }, message); yield* failsWithS(baseConfig({ api: { port: 0 } }), message); }), ), @@ -369,7 +383,7 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", isolated( Effect.gen(function* () { const message = "Missing required field in config: api.tls.key_path"; - failsWithL({ api: { tls: { enabled: true, cert_path: "kong.crt" } } }, message); + yield* failsWithL({ api: { tls: { enabled: true, cert_path: "kong.crt" } } }, message); yield* failsWithS( baseConfig({ api: { tls: { enabled: true, cert_path: "kong.crt" } } }), message, @@ -382,7 +396,7 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", isolated( Effect.gen(function* () { const message = "Invalid config for auth.jwt_secret. Must be at least 16 characters"; - failsWithL({ auth: { jwt_secret: "short" } }, message); + yield* failsWithL({ auth: { jwt_secret: "short" } }, message); yield* failsWithS(baseConfig({ auth: { jwt_secret: "short" } }), message); }), ), @@ -395,7 +409,7 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", Effect.gen(function* () { const message = "failed to parse config"; const auth = { service_role_key: "encrypted:not-a-real-ciphertext" }; - failsWithL({ auth }, message); + yield* failsWithL({ auth }, message); yield* failsWithS(baseConfig({ auth }), message); }), ), diff --git a/apps/cli/src/command-internal/config-validate.ts b/apps/cli/src/command-internal/config-validate.ts index 9549248426..4b49d32a7b 100644 --- a/apps/cli/src/command-internal/config-validate.ts +++ b/apps/cli/src/command-internal/config-validate.ts @@ -1,5 +1,4 @@ -import { statSync } from "node:fs"; -import { isAbsolute, join } from "node:path"; +import { Data, Effect, FileSystem, Path, type PlatformError } from "effect"; import { actionability, @@ -16,8 +15,8 @@ import { goUrlParse } from "./storage-url.ts"; * * - **D** = `db-config.toml-read.ts`: raw TOML document, Effect-based, fails with * `DbConfigLoadError`. - * - **L** = `local-config-values.ts`: decoded `@supabase/config` `CliConfig`, synchronous, - * throws `Error`. + * - **L** = `local-config-values.ts`: decoded `@supabase/config` `CliConfig`, Effect-based, + * fails with an `Error`. * * Per-command reimplementations of any branch {@link validateResolvedConfig} owns are forbidden; * hoist here instead. A few branches stay caller-only: `remotes[*].project_id` and @@ -62,12 +61,16 @@ export function parseGoBool(value: string): boolean | undefined { } /** - * Thrown by {@link validateResolvedConfig}. Does not override `.name` — it stays the inherited + * Raised by {@link validateResolvedConfig} (thrown) and by the `content_path`/secret resolvers + * (failed). Does not override `.name` — it stays the inherited * `"Error"` — so `instanceof Error` and `.name` checks can't distinguish it from a plain * `Error`; only `.message` is meant to be observed. */ -export class ConfigValidateError extends Error { +export class ConfigValidateError extends Data.Error<{ readonly message: string }> { static readonly [ErrorActionabilityFingerprintId] = "ConfigValidateError"; + constructor(message: string) { + super({ message }); + } get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return actionability.invalidConfig; } @@ -229,6 +232,10 @@ function messageOf(cause: unknown): string { return cause instanceof Error ? cause.message : String(cause); } +/** The `node:fs` error behind a `FileSystem` failure, whose message the config errors quote. */ +export const fsErrorCause = (error: PlatformError.PlatformError): unknown => + error.reason.cause ?? error.reason.description; + /** * The canonical `api.port` branch: an enabled API with a port of `0` is * invalid config. Exported so the storage-credentials resolver @@ -581,8 +588,14 @@ function isValidJson(value: string): boolean { } /** Absolute → verbatim; relative → join(workdir, "supabase", p). */ -export function resolveSigningKeysPath(workdir: string, signingKeysPath: string): string { - return isAbsolute(signingKeysPath) ? signingKeysPath : join(workdir, "supabase", signingKeysPath); +export function resolveSigningKeysPath( + path: Path.Path, + workdir: string, + signingKeysPath: string, +): string { + return path.isAbsolute(signingKeysPath) + ? signingKeysPath + : path.join(workdir, "supabase", signingKeysPath); } /** `failed to read signing keys: ${msg(cause)}` */ @@ -598,7 +611,7 @@ export function signingKeysDecodeErrorMessage(cause: unknown): string { // with the first key — that JWK-specific decode/signing logic stays in L. /** - * Pure exclusivity decision plus the path to read for one template/notification entry. Throws + * Exclusivity decision plus the path to read for one template/notification entry. Fails with * {@link ConfigValidateError} when `contentPath === ""` and `contentPresent` (`content_path` * is required instead of `content`). Returns the absolute, canonicalized path to read, or * `undefined` when there's nothing to read; a set `contentPath` always wins over `content` @@ -606,10 +619,10 @@ export function signingKeysDecodeErrorMessage(cause: unknown): string { * * The resolved candidate and `base` are both canonicalized and the candidate must resolve * inside `base` — an absolute path, a `..` escape, or an in-root symlink pointing outside the - * project root all throw. This applies unconditionally to every caller (config validation, + * project root all fail. This applies unconditionally to every caller (config validation, * `config push` content loading, `start`'s pre-Docker containment pass) — there is no opt-out. */ -export function resolveEmailTemplateContentPath(args: { +export const resolveEmailTemplateContentPath = Effect.fnUntraced(function* (args: { readonly section: "template" | "notification"; readonly name: string; /** Post-env-expand; `""` = absent. */ @@ -617,35 +630,36 @@ export function resolveEmailTemplateContentPath(args: { /** Raw `content` key present in the TOML document. */ readonly contentPresent: boolean; readonly base: string; -}): string | undefined { +}) { if (args.contentPath.length === 0) { if (args.contentPresent) { - throw new ConfigValidateError( + return yield* new ConfigValidateError( `Invalid config for auth.email.${args.section}.${args.name}.content: please use content_path instead`, ); } return undefined; } + const path = yield* Path.Path; const candidate = args.section === "notification" - ? resolveNotificationContentPath(args.base, args.contentPath) - : isAbsolute(args.contentPath) + ? yield* resolveNotificationContentPath(args.base, args.contentPath) + : path.isAbsolute(args.contentPath) ? args.contentPath - : join(args.base, args.contentPath); - const resolvedCanonical = canonicalPathForContainment(candidate); - const canonicalBase = canonicalPathForContainment(args.base); - if (!isPathContainedInRoot(canonicalBase, resolvedCanonical)) { + : path.join(args.base, args.contentPath); + const resolvedCanonical = yield* canonicalPathForContainment(candidate); + const canonicalBase = yield* canonicalPathForContainment(args.base); + if (!isPathContainedInRoot(path, canonicalBase, resolvedCanonical)) { // Echoes the declared value, not `resolvedCanonical` — the declared value is already known // to the user (it's literally in config.toml or an env override they set), while echoing // the symlink-dereferenced target back would let a hostile config probe where it resolves. - throw new ConfigValidateError( + return yield* new ConfigValidateError( `Invalid config for auth.email.${args.section}.${args.name}.content_path: ` + `"${args.contentPath}" resolves outside the project root ${args.base} — ` + `move the file inside the project, or use a relative path that stays inside it.`, ); } return resolvedCanonical; -} +}); /** * Notification `content_path` resolution with a legacy fallback: older scaffolds documented @@ -656,15 +670,19 @@ export function resolveEmailTemplateContentPath(args: { * {@link probeFile}). Shared by config validation, `config push` content loading, and the Kong * template mount builder so every consumer sees the same file. */ -function resolveNotificationContentPath(base: string, contentPath: string): string { - if (isAbsolute(contentPath)) return contentPath; - const resolved = join(base, contentPath); - if (probeFile(resolved) === "missing") { - const fallbackResolved = join(base, "supabase", contentPath); - if (probeFile(fallbackResolved) === "exists") return fallbackResolved; +const resolveNotificationContentPath = Effect.fnUntraced(function* ( + base: string, + contentPath: string, +) { + const path = yield* Path.Path; + if (path.isAbsolute(contentPath)) return contentPath; + const resolved = path.join(base, contentPath); + if ((yield* probeFile(resolved)) === "missing") { + const fallbackResolved = path.join(base, "supabase", contentPath); + if ((yield* probeFile(fallbackResolved)) === "exists") return fallbackResolved; } return resolved; -} +}); /** * Tri-state existence probe for the notification legacy-fallback decision above: `"exists"` @@ -675,15 +693,20 @@ function resolveNotificationContentPath(base: string, contentPath: string): stri * must only be selected on a confirmed `"exists"`. A single boolean can't express both * defaults, so the tri-state lets each caller choose. */ -function probeFile(path: string): "exists" | "missing" | "unknown" { - try { - const stats = statSync(path, { throwIfNoEntry: false }); - if (stats === undefined) return "missing"; - return stats.isFile() ? "exists" : "missing"; - } catch { - return "unknown"; - } -} +const probeFile = Effect.fnUntraced(function* (path: string) { + const fs = yield* FileSystem.FileSystem; + return yield* fs.stat(path).pipe( + Effect.map((info): "exists" | "missing" | "unknown" => + info.type === "File" ? "exists" : "missing", + ), + Effect.catchReason( + "PlatformError", + "NotFound", + () => Effect.succeed("missing" as const), + () => Effect.succeed("unknown" as const), + ), + ); +}); /** `Invalid config for auth.email.${section}.${name}.content_path: ${msg(cause)}` */ export function emailContentPathReadErrorMessage( @@ -697,8 +720,8 @@ export function emailContentPathReadErrorMessage( // ── api.tls cert/key (path rule: NO isAbsolute guard) ── /** Unconditional join(workdir, "supabase", p) — `path.Join` absorbs a leading "/" too. */ -export function resolveApiTlsPath(workdir: string, p: string): string { - return join(workdir, "supabase", p); +export function resolveApiTlsPath(path: Path.Path, workdir: string, p: string): string { + return path.join(workdir, "supabase", p); } /** `failed to read TLS cert: ${msg(cause)}` */ diff --git a/apps/cli/src/command-internal/config-validate.unit.test.ts b/apps/cli/src/command-internal/config-validate.unit.test.ts index 6140bcd76f..9e9917bc23 100644 --- a/apps/cli/src/command-internal/config-validate.unit.test.ts +++ b/apps/cli/src/command-internal/config-validate.unit.test.ts @@ -1,16 +1,7 @@ -import { - chmodSync, - mkdirSync, - mkdtempSync, - readdirSync, - realpathSync, - rmSync, - symlinkSync, - writeFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; -import { join, relative } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it, layer } from "@effect/vitest"; +import { Effect, FileSystem, Path, PlatformError, Predicate } from "effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; import { BUCKET_NAME_PATTERN, @@ -106,33 +97,31 @@ describe("CLERK_DOMAIN_PATTERN", () => { // Direct coverage for the containment check shared by every caller (config push's // loadAuthEmailContent, db-config.toml-read.ts, local-config-values.ts, start.handler.ts's // pre-Docker pass), cheaper to pin here than to re-derive through every caller's own fixtures. -describe("resolveEmailTemplateContentPath", () => { - let projectRoot = ""; - let outsideDir = ""; - - afterEach(() => { - if (projectRoot.length > 0) { - rmSync(projectRoot, { recursive: true, force: true }); - projectRoot = ""; - } - if (outsideDir.length > 0) { - rmSync(outsideDir, { recursive: true, force: true }); - outsideDir = ""; - } - }); +layer(BunServices.layer)("resolveEmailTemplateContentPath", (it) => { + const tempDir = (prefix: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + return yield* fs.makeTempDirectoryScoped({ prefix }); + }); - function setup(): string { - projectRoot = mkdtempSync(join(tmpdir(), "config-validate-email-content-")); - return projectRoot; - } + const setup = () => tempDir("config-validate-email-content-"); /** A real file outside `base`, so a containment test proves the escape check fires rather than a missing-file error. */ - function setupOutsideFile(): string { - outsideDir = mkdtempSync(join(tmpdir(), "config-validate-email-content-outside-")); - const outsideFile = join(outsideDir, "secret.html"); - writeFileSync(outsideFile, "

Outside

"); + const setupOutsideFile = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const outsideDir = yield* tempDir("config-validate-email-content-outside-"); + const outsideFile = path.join(outsideDir, "secret.html"); + yield* fs.writeFileString(outsideFile, "

Outside

"); return outsideFile; - } + }); + + /** Whether `chmod 000` actually blocks reading `dir` here (it doesn't when running as root). */ + const permissionEnforced = (dir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + return !(yield* fs.readDirectory(dir).pipe(Effect.isSuccess)); + }); function resolveContentPath( section: "template" | "notification", @@ -148,252 +137,361 @@ describe("resolveEmailTemplateContentPath", () => { }); } - it.each(["template", "notification"] as const)( + it.effect.each(["template", "notification"] as const)( "rejects an absolute %s content_path outside the project root", - (section) => { - const base = setup(); - const outsideFile = setupOutsideFile(); + (section) => + Effect.gen(function* () { + const base = yield* setup(); + const outsideFile = yield* setupOutsideFile; - expect(() => resolveContentPath(section, outsideFile, base)).toThrow(ConfigValidateError); - expect(() => resolveContentPath(section, outsideFile, base)).toThrow( - /resolves outside the project root/, - ); - }, + expect(yield* Effect.flip(resolveContentPath(section, outsideFile, base))).toBeInstanceOf( + ConfigValidateError, + ); + expect( + (yield* Effect.flip(resolveContentPath(section, outsideFile, base))).message, + ).toMatch(/resolves outside the project root/); + }), ); - it.each(["template", "notification"] as const)( + it.effect.each(["template", "notification"] as const)( "rejects a relative %s content_path that escapes the project root via ..", - (section) => { - const base = setup(); - const outsideFile = setupOutsideFile(); - const escapePath = relative(base, outsideFile); - - expect(() => resolveContentPath(section, escapePath, base)).toThrow( - /resolves outside the project root/, - ); - }, + (section) => + Effect.gen(function* () { + const path = yield* Path.Path; + const base = yield* setup(); + const outsideFile = yield* setupOutsideFile; + const escapePath = path.relative(base, outsideFile); + + expect((yield* Effect.flip(resolveContentPath(section, escapePath, base))).message).toMatch( + /resolves outside the project root/, + ); + }), ); - it.each(["template", "notification"] as const)( + it.effect.each(["template", "notification"] as const)( "rejects a %s content_path that is an in-root symlink pointing outside the project root", - (section) => { - const base = setup(); - const outsideFile = setupOutsideFile(); - const symlinkPath = join(base, "evil.html"); - symlinkSync(outsideFile, symlinkPath); - - expect(() => resolveContentPath(section, "./evil.html", base)).toThrow( - /resolves outside the project root/, - ); - }, + (section) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const base = yield* setup(); + const outsideFile = yield* setupOutsideFile; + const symlinkPath = path.join(base, "evil.html"); + yield* fs.symlink(outsideFile, symlinkPath); + + expect( + (yield* Effect.flip(resolveContentPath(section, "./evil.html", base))).message, + ).toMatch(/resolves outside the project root/); + }), ); - it("accepts an in-root sibling path whose name literally starts with two dots, distinct from a .. escape", () => { - const base = setup(); - const dotDir = join(base, "..templates"); - mkdirSync(dotDir, { recursive: true }); - writeFileSync(join(dotDir, "invite.html"), "

Invite

"); - - const resolved = resolveContentPath("template", "..templates/invite.html", base); - - expect(resolved).toBe(join(realpathSync(base), "..templates", "invite.html")); - }); + it.effect( + "accepts an in-root sibling path whose name literally starts with two dots, distinct from a .. escape", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const base = yield* setup(); + const dotDir = path.join(base, "..templates"); + yield* fs.makeDirectory(dotDir, { recursive: true }); + yield* fs.writeFileString(path.join(dotDir, "invite.html"), "

Invite

"); + + const resolved = yield* resolveContentPath("template", "..templates/invite.html", base); + + expect(resolved).toBe(path.join(yield* fs.realPath(base), "..templates", "invite.html")); + }), + ); - it("accepts a content_path that resolves to exactly the project root", () => { - const base = setup(); + it.effect("accepts a content_path that resolves to exactly the project root", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const base = yield* setup(); - const resolved = resolveContentPath("template", ".", base); + const resolved = yield* resolveContentPath("template", ".", base); - expect(resolved).toBe(realpathSync(base)); - }); + expect(resolved).toBe(yield* fs.realPath(base)); + }), + ); - it("resolves a missing in-root file behind a symlinked project root instead of raising the containment error", () => { - const realDir = mkdtempSync(join(tmpdir(), "config-validate-email-content-real-")); - const linkContainer = mkdtempSync(join(tmpdir(), "config-validate-email-content-link-")); - const symlinkedRoot = join(linkContainer, "project-root"); - symlinkSync(realDir, symlinkedRoot, "dir"); - - try { - const resolved = resolveContentPath("template", "missing-invite.html", symlinkedRoot); - expect(resolved).toBe(join(realpathSync(symlinkedRoot), "missing-invite.html")); - } finally { - rmSync(linkContainer, { recursive: true, force: true }); - rmSync(realDir, { recursive: true, force: true }); - } - }); + it.effect( + "resolves a missing in-root file behind a symlinked project root instead of raising the containment error", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const realDir = yield* tempDir("config-validate-email-content-real-"); + const linkContainer = yield* tempDir("config-validate-email-content-link-"); + const symlinkedRoot = path.join(linkContainer, "project-root"); + yield* fs.symlink(realDir, symlinkedRoot); + + const resolved = yield* resolveContentPath( + "template", + "missing-invite.html", + symlinkedRoot, + ); + expect(resolved).toBe(path.join(yield* fs.realPath(symlinkedRoot), "missing-invite.html")); + }), + ); - it.each(["template", "notification"] as const)( + it.effect.each(["template", "notification"] as const)( "rejects a %s content_path that is an in-root dangling symlink pointing to a nonexistent target outside the project root", - (section) => { - const base = setup(); - outsideDir = mkdtempSync(join(tmpdir(), "config-validate-email-content-outside-")); - const neverCreatedOutsideTarget = join(outsideDir, "never-created.html"); - const danglingSymlinkPath = join(base, "dangling.html"); - symlinkSync(neverCreatedOutsideTarget, danglingSymlinkPath); - - expect(() => resolveContentPath(section, "./dangling.html", base)).toThrow( - ConfigValidateError, - ); - expect(() => resolveContentPath(section, "./dangling.html", base)).toThrow( - /resolves outside the project root/, - ); - }, + (section) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const base = yield* setup(); + const outsideDir = yield* tempDir("config-validate-email-content-outside-"); + const neverCreatedOutsideTarget = path.join(outsideDir, "never-created.html"); + const danglingSymlinkPath = path.join(base, "dangling.html"); + yield* fs.symlink(neverCreatedOutsideTarget, danglingSymlinkPath); + + expect( + yield* Effect.flip(resolveContentPath(section, "./dangling.html", base)), + ).toBeInstanceOf(ConfigValidateError); + expect( + (yield* Effect.flip(resolveContentPath(section, "./dangling.html", base))).message, + ).toMatch(/resolves outside the project root/); + }), ); - it.each(["template", "notification"] as const)( - "rejects a %s content_path that is an in-root symlink whose outside target sits behind an unsearchable (EACCES) directory", - (section) => { - // Skipped when this environment doesn't enforce chmod 000 (e.g. running as root); the - // dangling-symlink case above already covers the core regression without a permission trick. - const base = setup(); - outsideDir = mkdtempSync(join(tmpdir(), "config-validate-email-content-outside-")); - const unsearchableDir = join(outsideDir, "locked"); - mkdirSync(unsearchableDir); - const target = join(unsearchableDir, "secret.html"); - writeFileSync(target, "

Locked

"); - chmodSync(unsearchableDir, 0o000); - - try { - let permissionEnforced = true; - try { - readdirSync(unsearchableDir); - permissionEnforced = false; - } catch { - // expected in a normal, unprivileged environment — confirms chmod 000 actually blocks access here. - } - if (!permissionEnforced) { + it.effect.each(["template", "notification"] as const)( + "rejects a %s content_path that is an in-root dangling symlink whose own mode denies reading it", + (section) => + Effect.gen(function* () { + /* Skipped where symlink modes can't be set or aren't enforced (Linux, root). */ + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const base = yield* setup(); + const outsideDir = yield* tempDir("config-validate-email-content-outside-"); + const lockedSymlinkPath = path.join(base, "locked.html"); + yield* fs.symlink(path.join(outsideDir, "never-created.html"), lockedSymlinkPath); + const locked = yield* spawner + .exitCode(ChildProcess.make("chmod", ["-h", "000", lockedSymlinkPath])) + .pipe( + Effect.map((exitCode) => exitCode === 0), + Effect.orElseSucceed(() => false), + ); + if (!locked || (yield* fs.readLink(lockedSymlinkPath).pipe(Effect.isSuccess))) { return; } - const symlinkPath = join(base, "unsearchable.html"); - symlinkSync(target, symlinkPath); + const failure = yield* Effect.flip(resolveContentPath(section, "./locked.html", base)); + expect( + failure instanceof PlatformError.PlatformError && + Predicate.isTagged(failure.reason, "PermissionDenied"), + ).toBe(true); + }), + ); - expect(() => resolveContentPath(section, "./unsearchable.html", base)).toThrow( - ConfigValidateError, - ); - expect(() => resolveContentPath(section, "./unsearchable.html", base)).toThrow( - /resolves outside the project root/, - ); - } finally { - chmodSync(unsearchableDir, 0o755); - } - }, + /* Linux ignores a symlink's own mode, so a `readLink` denied by the symlink itself is driven + through a `FileSystem` that denies it, with the parent `stat` passing or failing. */ + const denyingReadLink = (lockedPath: string, parentSearchable: boolean) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const parentProbe = `${path.dirname(lockedPath)}${path.sep}.`; + const parentInfo = yield* fs.stat(parentProbe); + const denied = (method: string, pathOrDescriptor: string) => + PlatformError.systemError({ + _tag: "PermissionDenied", + module: "FileSystem", + method, + pathOrDescriptor, + }); + return FileSystem.FileSystem.of({ + ...fs, + readLink: (p) => (p === lockedPath ? Effect.fail(denied("readLink", p)) : fs.readLink(p)), + stat: (p) => + p !== parentProbe + ? fs.stat(p) + : parentSearchable + ? Effect.succeed(parentInfo) + : Effect.fail(denied("stat", p)), + }); + }); + + it.effect("rejects a content_path whose own readLink is denied under a searchable parent", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const base = yield* setup(); + const lockedFs = yield* denyingReadLink(path.join(base, "locked.html"), true); + + const failure = yield* Effect.flip( + resolveContentPath("template", "./locked.html", base).pipe( + Effect.provideService(FileSystem.FileSystem, lockedFs), + ), + ); + expect( + failure instanceof PlatformError.PlatformError && + Predicate.isTagged(failure.reason, "PermissionDenied"), + ).toBe(true); + }), ); - it("accepts a genuinely in-root file behind an unsearchable (EACCES) directory, even when the project root itself is reached through a symlink", () => { - const realDir = mkdtempSync(join(tmpdir(), "config-validate-email-content-real-")); - const linkContainer = mkdtempSync(join(tmpdir(), "config-validate-email-content-link-")); - const symlinkedRoot = join(linkContainer, "project-root"); - symlinkSync(realDir, symlinkedRoot, "dir"); - const lockedDir = join(realDir, "locked"); - mkdirSync(lockedDir); - const target = join(lockedDir, "invite.html"); - writeFileSync(target, "

Invite

"); - chmodSync(lockedDir, 0o000); - - try { - let permissionEnforced = true; - try { - readdirSync(lockedDir); - permissionEnforced = false; - } catch { - // expected in a normal, unprivileged environment — confirms chmod 000 actually blocks access here. - } - if (!permissionEnforced) { - return; - } - - const resolved = resolveContentPath("template", "./locked/invite.html", symlinkedRoot); - - expect(resolved).toBe(join(realpathSync(symlinkedRoot), "locked", "invite.html")); - } finally { - chmodSync(lockedDir, 0o755); - rmSync(linkContainer, { recursive: true, force: true }); - rmSync(realDir, { recursive: true, force: true }); - } - }); + it.effect("accepts a content_path whose readLink is denied by an unsearchable parent", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const base = yield* setup(); + const lockedFs = yield* denyingReadLink(path.join(base, "locked.html"), false); + + const resolved = yield* resolveContentPath("template", "./locked.html", base).pipe( + Effect.provideService(FileSystem.FileSystem, lockedFs), + ); + expect(resolved).toBe(path.join(yield* fs.realPath(base), "locked.html")); + }), + ); - it.each(["template", "notification"] as const)( + it.effect.each(["template", "notification"] as const)( + "rejects a %s content_path that is an in-root symlink whose outside target sits behind an unsearchable (EACCES) directory", + (section) => + Effect.gen(function* () { + // Skipped when this environment doesn't enforce chmod 000 (e.g. running as root); the + // dangling-symlink case above already covers the core regression without a permission trick. + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const base = yield* setup(); + const outsideDir = yield* tempDir("config-validate-email-content-outside-"); + const unsearchableDir = path.join(outsideDir, "locked"); + yield* fs.makeDirectory(unsearchableDir); + const target = path.join(unsearchableDir, "secret.html"); + yield* fs.writeFileString(target, "

Locked

"); + yield* fs.chmod(unsearchableDir, 0o000); + + yield* Effect.gen(function* () { + if (!(yield* permissionEnforced(unsearchableDir))) { + return; + } + + const symlinkPath = path.join(base, "unsearchable.html"); + yield* fs.symlink(target, symlinkPath); + + expect( + yield* Effect.flip(resolveContentPath(section, "./unsearchable.html", base)), + ).toBeInstanceOf(ConfigValidateError); + expect( + (yield* Effect.flip(resolveContentPath(section, "./unsearchable.html", base))).message, + ).toMatch(/resolves outside the project root/); + }).pipe(Effect.ensuring(fs.chmod(unsearchableDir, 0o755).pipe(Effect.orDie))); + }), + ); + + it.effect( + "accepts a genuinely in-root file behind an unsearchable (EACCES) directory, even when the project root itself is reached through a symlink", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const realDir = yield* tempDir("config-validate-email-content-real-"); + const linkContainer = yield* tempDir("config-validate-email-content-link-"); + const symlinkedRoot = path.join(linkContainer, "project-root"); + yield* fs.symlink(realDir, symlinkedRoot); + const lockedDir = path.join(realDir, "locked"); + yield* fs.makeDirectory(lockedDir); + const target = path.join(lockedDir, "invite.html"); + yield* fs.writeFileString(target, "

Invite

"); + yield* fs.chmod(lockedDir, 0o000); + + yield* Effect.gen(function* () { + if (!(yield* permissionEnforced(lockedDir))) { + return; + } + + const resolved = yield* resolveContentPath( + "template", + "./locked/invite.html", + symlinkedRoot, + ); + + expect(resolved).toBe( + path.join(yield* fs.realPath(symlinkedRoot), "locked", "invite.html"), + ); + }).pipe(Effect.ensuring(fs.chmod(lockedDir, 0o755).pipe(Effect.orDie))); + }), + ); + + it.effect.each(["template", "notification"] as const)( "rejects a %s content_path that is an in-root symlink pointing to an unstattable (ENAMETOOLONG) target name, without relying on directory permissions", - (section) => { - const base = setup(); - outsideDir = mkdtempSync(join(tmpdir(), "config-validate-email-content-outside-")); - const tooLongName = `${"a".repeat(300)}.html`; - const symlinkPath = join(base, "toolong.html"); - try { - symlinkSync(join(outsideDir, tooLongName), symlinkPath); - } catch { + (section) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const base = yield* setup(); + const outsideDir = yield* tempDir("config-validate-email-content-outside-"); + const tooLongName = `${"a".repeat(300)}.html`; + const symlinkPath = path.join(base, "toolong.html"); // Unprivileged symlink creation isn't unconditionally available (e.g. Windows without // Developer Mode/admin) — skip rather than fail an environment that can't set this up. - return; - } + if ( + !(yield* fs + .symlink(path.join(outsideDir, tooLongName), symlinkPath) + .pipe(Effect.isSuccess)) + ) { + return; + } - expect(() => resolveContentPath(section, "./toolong.html", base)).toThrow( - ConfigValidateError, - ); - expect(() => resolveContentPath(section, "./toolong.html", base)).toThrow( - /resolves outside the project root/, - ); - }, + expect( + yield* Effect.flip(resolveContentPath(section, "./toolong.html", base)), + ).toBeInstanceOf(ConfigValidateError); + expect( + (yield* Effect.flip(resolveContentPath(section, "./toolong.html", base))).message, + ).toMatch(/resolves outside the project root/); + }), ); - it.each(["template", "notification"] as const)( + it.effect.each(["template", "notification"] as const)( "rejects an in-root %s symlink loop instead of hanging or crashing", - (section) => { - // Reuses the symlinked-root fixture (rather than relying on incidental ambient-tmpdir - // symlinks) so there's a guaranteed canonicalization gap between root and the loop path. - const realDir = mkdtempSync(join(tmpdir(), "config-validate-email-content-real-")); - const linkContainer = mkdtempSync(join(tmpdir(), "config-validate-email-content-link-")); - const symlinkedRoot = join(linkContainer, "project-root"); - symlinkSync(realDir, symlinkedRoot, "dir"); - - try { - const loopA = join(symlinkedRoot, "loop-a.html"); - const loopB = join(symlinkedRoot, "loop-b.html"); - symlinkSync(loopB, loopA); - symlinkSync(loopA, loopB); - - expect(() => resolveContentPath(section, "./loop-a.html", symlinkedRoot)).toThrow( - ConfigValidateError, - ); - expect(() => resolveContentPath(section, "./loop-a.html", symlinkedRoot)).toThrow( - /resolves outside the project root/, - ); - } finally { - rmSync(linkContainer, { recursive: true, force: true }); - rmSync(realDir, { recursive: true, force: true }); - } - }, + (section) => + Effect.gen(function* () { + // Reuses the symlinked-root fixture (rather than relying on incidental ambient-tmpdir + // symlinks) so there's a guaranteed canonicalization gap between root and the loop path. + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const realDir = yield* tempDir("config-validate-email-content-real-"); + const linkContainer = yield* tempDir("config-validate-email-content-link-"); + const symlinkedRoot = path.join(linkContainer, "project-root"); + yield* fs.symlink(realDir, symlinkedRoot); + + const loopA = path.join(symlinkedRoot, "loop-a.html"); + const loopB = path.join(symlinkedRoot, "loop-b.html"); + yield* fs.symlink(loopB, loopA); + yield* fs.symlink(loopA, loopB); + + expect( + yield* Effect.flip(resolveContentPath(section, "./loop-a.html", symlinkedRoot)), + ).toBeInstanceOf(ConfigValidateError); + expect( + (yield* Effect.flip(resolveContentPath(section, "./loop-a.html", symlinkedRoot))).message, + ).toMatch(/resolves outside the project root/); + }), ); - it("never selects an unstattable notification legacy-fallback twin behind an unsearchable (EACCES) supabase/ directory", () => { - const base = setup(); - const supabaseDir = join(base, "supabase"); - mkdirSync(supabaseDir); - chmodSync(supabaseDir, 0o000); - - try { - let permissionEnforced = true; - try { - readdirSync(supabaseDir); - permissionEnforced = false; - } catch { - // expected in a normal, unprivileged environment — confirms chmod 000 actually blocks access here. - } - if (!permissionEnforced) { - return; - } - - const resolved = resolveContentPath("notification", "notification.html", base); - - // The result must be the root-resolved path, not the unverified legacy twin; this - // function only resolves a path, it doesn't check that the file exists, so a non-throw - // here is expected. - expect(resolved).toBe(join(realpathSync(base), "notification.html")); - } finally { - chmodSync(supabaseDir, 0o755); - } - }); + it.effect( + "never selects an unstattable notification legacy-fallback twin behind an unsearchable (EACCES) supabase/ directory", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const base = yield* setup(); + const supabaseDir = path.join(base, "supabase"); + yield* fs.makeDirectory(supabaseDir); + yield* fs.chmod(supabaseDir, 0o000); + + yield* Effect.gen(function* () { + if (!(yield* permissionEnforced(supabaseDir))) { + return; + } + + const resolved = yield* resolveContentPath("notification", "notification.html", base); + + // The result must be the root-resolved path, not the unverified legacy twin; this + // function only resolves a path, it doesn't check that the file exists, so a non-throw + // here is expected. + expect(resolved).toBe(path.join(yield* fs.realPath(base), "notification.html")); + }).pipe(Effect.ensuring(fs.chmod(supabaseDir, 0o755).pipe(Effect.orDie))); + }), + ); }); /** diff --git a/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts b/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts index 067ce31ae0..cbcba5b4f6 100644 --- a/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts +++ b/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts @@ -67,26 +67,24 @@ export interface DbBootstrapConfig { readonly storageTargetMigration: string; } +const configOverrideMessage = (dottedFieldPath: string, cause: unknown) => + `invalid config for ${dottedFieldPath}: ${cause instanceof Error ? cause.message : String(cause)}`; + /** - * Wraps a synchronous `envOverride*` read that throws on a malformed value into a typed failure - * instead of an untyped Effect defect. + * Maps an `envOverride*` read's malformed-value failure into the caller's typed error instead of + * an untyped Effect defect. * * @param dottedFieldPath - Config path embedded in the error message (`invalid config for * : `). */ -function wrapConfigOverride( +const wrapConfigOverride = ( dottedFieldPath: string, - thunk: () => T, + effect: Effect.Effect, mapConfigError: (message: string) => E, -): Effect.Effect { - return Effect.try({ - try: thunk, - catch: (cause) => - mapConfigError( - `invalid config for ${dottedFieldPath}: ${cause instanceof Error ? cause.message : String(cause)}`, - ), - }); -} +): Effect.Effect => + effect.pipe( + Effect.mapError((cause) => mapConfigError(configOverrideMessage(dottedFieldPath, cause))), + ); /** * Resolves every field {@link startDatabase} needs from `config`/`projectEnvValues`: values @@ -111,33 +109,39 @@ export const resolveDbBootstrapConfig = ( // wins over a conflicting SUPABASE_DB_MAJOR_VERSION. const majorVersion = remoteWins("db.major_version") ? config.db.major_version - : envOverrideMajorVersion(config.db.major_version, projectEnvValues); + : yield* envOverrideMajorVersion(config.db.major_version, projectEnvValues).pipe( + Effect.orDie, + ); // orioledb_version and the four S3 fields feed the Postgres container's image/env directly. - // `envOverride` never throws, so these don't need `wrapConfigOverride`. Same remote-over-env + // `envOverride` never fails, so these don't need `wrapConfigOverride`. Same remote-over-env // precedence as `majorVersion` applies to each. const orioledbVersion = remoteWins("db.orioledb_version") ? config.db.orioledb_version - : envOverrideOrioleDBVersion(config.db.orioledb_version, projectEnvValues); + : yield* envOverrideOrioleDBVersion(config.db.orioledb_version, projectEnvValues); const s3Host = remoteWins("experimental.s3_host") ? config.experimental.s3_host - : envOverride("SUPABASE_EXPERIMENTAL_S3_HOST", config.experimental.s3_host, projectEnvValues); + : yield* envOverride( + "SUPABASE_EXPERIMENTAL_S3_HOST", + config.experimental.s3_host, + projectEnvValues, + ); const s3Region = remoteWins("experimental.s3_region") ? config.experimental.s3_region - : envOverride( + : yield* envOverride( "SUPABASE_EXPERIMENTAL_S3_REGION", config.experimental.s3_region, projectEnvValues, ); const s3AccessKey = remoteWins("experimental.s3_access_key") ? config.experimental.s3_access_key - : envOverride( + : yield* envOverride( "SUPABASE_EXPERIMENTAL_S3_ACCESS_KEY", config.experimental.s3_access_key, projectEnvValues, ); const s3SecretKey = remoteWins("experimental.s3_secret_key") ? config.experimental.s3_secret_key - : envOverride( + : yield* envOverride( "SUPABASE_EXPERIMENTAL_S3_SECRET_KEY", config.experimental.s3_secret_key, projectEnvValues, @@ -148,41 +152,38 @@ export const resolveDbBootstrapConfig = ( // malformed override; it has no `--exclude`/gates equivalent of its own. const realtimeEnabledForSetup = yield* wrapConfigOverride( "realtime.enabled", - () => - remoteWins("realtime.enabled") - ? config.realtime.enabled - : envOverrideBool( - "SUPABASE_REALTIME_ENABLED", - config.realtime.enabled, - "realtime.enabled", - projectEnvValues, - ), + remoteWins("realtime.enabled") + ? Effect.succeed(config.realtime.enabled) + : envOverrideBool( + "SUPABASE_REALTIME_ENABLED", + config.realtime.enabled, + "realtime.enabled", + projectEnvValues, + ), mapConfigError, ); const storageEnabledForSetup = yield* wrapConfigOverride( "storage.enabled", - () => - remoteWins("storage.enabled") - ? config.storage.enabled - : envOverrideBool( - "SUPABASE_STORAGE_ENABLED", - config.storage.enabled, - "storage.enabled", - projectEnvValues, - ), + remoteWins("storage.enabled") + ? Effect.succeed(config.storage.enabled) + : envOverrideBool( + "SUPABASE_STORAGE_ENABLED", + config.storage.enabled, + "storage.enabled", + projectEnvValues, + ), mapConfigError, ); const authEnabledForSetup = yield* wrapConfigOverride( "auth.enabled", - () => - remoteWins("auth.enabled") - ? config.auth.enabled - : envOverrideBool( - "SUPABASE_AUTH_ENABLED", - config.auth.enabled, - "auth.enabled", - projectEnvValues, - ), + remoteWins("auth.enabled") + ? Effect.succeed(config.auth.enabled) + : envOverrideBool( + "SUPABASE_AUTH_ENABLED", + config.auth.enabled, + "auth.enabled", + projectEnvValues, + ), mapConfigError, ); @@ -190,27 +191,29 @@ export const resolveDbBootstrapConfig = ( // overridden value. const realtimeIpVersion = yield* wrapConfigOverride( "realtime.ip_version", - () => { - // `envOverrideRealtimeIpVersion` reads `process.env` unconditionally, so it can't be + Effect.gen(function* () { + // `envOverrideRealtimeIpVersion` reads the environment unconditionally, so it can't be // called at all when the remote block wins — a raw env var would still beat it. The - // throw below only narrows the type; the schema already guarantees one of these values. + // failure below only narrows the type; the schema already guarantees one of these values. if (remoteWins("realtime.ip_version")) { const value = config.realtime.ip_version; if (value !== "IPv4" && value !== "IPv6") { - throw new InvalidRealtimeIpVersionEnvOverrideError("realtime.ip_version", value); + return yield* new InvalidRealtimeIpVersionEnvOverrideError( + "realtime.ip_version", + value, + ); } return value; } - return envOverrideRealtimeIpVersion(config.realtime.ip_version, projectEnvValues); - }, + return yield* envOverrideRealtimeIpVersion(config.realtime.ip_version, projectEnvValues); + }), mapConfigError, ); const realtimeMaxHeaderLength = yield* wrapConfigOverride( "realtime.max_header_length", - () => - remoteWins("realtime.max_header_length") - ? config.realtime.max_header_length - : envOverrideRealtimeMaxHeaderLength(config.realtime.max_header_length, projectEnvValues), + remoteWins("realtime.max_header_length") + ? Effect.succeed(config.realtime.max_header_length) + : envOverrideRealtimeMaxHeaderLength(config.realtime.max_header_length, projectEnvValues), mapConfigError, ); @@ -219,16 +222,15 @@ export const resolveDbBootstrapConfig = ( // validated eagerly here rather than surfacing later inside a container env builder. const storageFileSizeLimit = remoteWins("storage.file_size_limit") ? config.storage.file_size_limit - : (envOverride( + : ((yield* envOverride( "SUPABASE_STORAGE_FILE_SIZE_LIMIT", config.storage.file_size_limit, projectEnvValues, - ) ?? config.storage.file_size_limit); - yield* wrapConfigOverride( - "storage.file_size_limit", - () => ramInBytes(storageFileSizeLimit), - mapConfigError, - ); + )) ?? config.storage.file_size_limit); + yield* Effect.try({ + try: () => ramInBytes(storageFileSizeLimit), + catch: (cause) => mapConfigError(configOverrideMessage("storage.file_size_limit", cause)), + }); // Reads a `supabase/.temp/postgres-version` pin written by `supabase link`; a missing or // unreadable pin falls back to the embedded default, so this never fails. @@ -250,7 +252,11 @@ export const resolveDbBootstrapConfig = ( const dbHealthTimeout = remoteWins("db.health_timeout") ? config.db.health_timeout - : envOverride("SUPABASE_DB_HEALTH_TIMEOUT", config.db.health_timeout, projectEnvValues); + : yield* envOverride( + "SUPABASE_DB_HEALTH_TIMEOUT", + config.db.health_timeout, + projectEnvValues, + ); const dbHealthTimeoutSeconds = yield* Effect.try({ try: () => resolveHealthTimeoutSeconds(dbHealthTimeout ?? config.db.health_timeout), catch: (cause) => diff --git a/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.ts b/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.ts index ebf52e7904..c03ebf4fdb 100644 --- a/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.ts +++ b/apps/cli/src/command-internal/db-bootstrap/local-container-inputs.ts @@ -113,18 +113,16 @@ export const buildLocalDbContainerInputs = ( const { config, projectEnvValues, loaded, hostname, projectId } = context; const experimental = yield* resolveExperimentalWithProjectEnv(projectEnvValues); - const values = yield* Effect.try({ - try: () => - resolveLocalConfigValues( - config, - hostname, - workdir, - projectEnvValues, - loaded?.document, - remoteOverrideKeys, - ), - catch: (cause) => mapError(cause instanceof Error ? cause.message : String(cause)), - }); + const values = yield* resolveLocalConfigValues( + config, + hostname, + workdir, + projectEnvValues, + loaded?.document, + remoteOverrideKeys, + ).pipe( + Effect.mapError((cause) => mapError(cause instanceof Error ? cause.message : String(cause))), + ); const bootstrapConfig = yield* resolveDbBootstrapConfig( fs, @@ -160,11 +158,11 @@ export const buildLocalDbContainerInputs = ( port: values.dbPort, major_version: bootstrapConfig.majorVersion, orioledb_version: bootstrapConfig.orioledbVersion, - settings: resolveDbSettingsEnvOverrides( + settings: yield* resolveDbSettingsEnvOverrides( config.db.settings, projectEnvValues, remoteOverrideKeys, - ), + ).pipe(Effect.orDie), }, experimental: { ...config.experimental, @@ -223,10 +221,12 @@ export const buildLocalDbContainerInputs = ( remoteOverrideKeys, ).pipe( Effect.provideService(HttpClient.HttpClient, httpClient), + Effect.provideService(FileSystem.FileSystem, fs), + Effect.provideService(Path.Path, path), Effect.mapError((cause) => mapError(cause.message)), ), apiUrl: values.apiUrl, - authExternalUrl: resolveAuthExternalUrl( + authExternalUrl: yield* resolveAuthExternalUrl( loaded?.document, projectEnvValues, remoteOverrideKeys, diff --git a/apps/cli/src/command-internal/db-bootstrap/start-local-database.ts b/apps/cli/src/command-internal/db-bootstrap/start-local-database.ts index 2942c5e4b2..6997ef6b94 100644 --- a/apps/cli/src/command-internal/db-bootstrap/start-local-database.ts +++ b/apps/cli/src/command-internal/db-bootstrap/start-local-database.ts @@ -61,23 +61,22 @@ function asRecord(value: unknown): Record | undefined { : undefined; } +const dbConfigOverrideError = (dottedFieldPath: string) => (cause: unknown) => + new DbConfigLoadError({ + message: `invalid config for ${dottedFieldPath}: ${cause instanceof Error ? cause.message : String(cause)}`, + }); + /** - * Wraps a synchronous resolver/parser that throws on a malformed config value into a typed - * `DbConfigLoadError` failure — mirrors `commands/start/start.handler.ts`'s identical - * `wrapConfigOverride`: config loading hard-fails on a bad decode before any Docker work runs. + * Maps a config resolver's malformed-value failure into a typed `DbConfigLoadError` — mirrors + * `commands/start/start.handler.ts`'s identical `wrapConfigOverride`: config loading hard-fails + * on a bad decode before any Docker work runs. */ -function wrapDbConfigOverride( - dottedFieldPath: string, - thunk: () => T, -): Effect.Effect { - return Effect.try({ - try: thunk, - catch: (cause) => - new DbConfigLoadError({ - message: `invalid config for ${dottedFieldPath}: ${cause instanceof Error ? cause.message : String(cause)}`, - }), - }); -} +const wrapDbConfigOverride = (dottedFieldPath: string, effect: Effect.Effect) => + effect.pipe(Effect.mapError(dbConfigOverrideError(dottedFieldPath))); + +/** {@link wrapDbConfigOverride} for a synchronous parser that throws on a malformed value. */ +const wrapDbConfigParse = (dottedFieldPath: string, thunk: () => T) => + Effect.try({ try: thunk, catch: dbConfigOverrideError(dottedFieldPath) }); type StartLocalDatabaseStatus = "already-running" | "started"; @@ -133,27 +132,30 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu // touched or the already-running check runs. The parsed values are discarded; only the // fail-fast behavior matters. const authDocForValidation = asRecord(loaded?.document?.["auth"]); - const resolvedEmailForValidation = yield* wrapDbConfigOverride("auth.email", () => + const resolvedEmailForValidation = yield* wrapDbConfigOverride( + "auth.email", resolveAuthEmail(config.auth.email, authDocForValidation, projectEnvValues), ); - yield* wrapDbConfigOverride("auth.email.max_frequency", () => + yield* wrapDbConfigParse("auth.email.max_frequency", () => parseGoDuration(resolvedEmailForValidation.max_frequency), ); - yield* wrapDbConfigOverride("auth.email.smtp", () => + yield* wrapDbConfigOverride( + "auth.email.smtp", resolveAuthEmailSmtp(authDocForValidation, projectEnvValues), ); - const smsForValidation = yield* wrapDbConfigOverride("auth.sms", () => + const smsForValidation = yield* wrapDbConfigOverride( + "auth.sms", resolveAuthSms(authDocForValidation, config.auth.sms, projectEnvValues), ); - yield* wrapDbConfigOverride("auth.sms.max_frequency", () => + yield* wrapDbConfigParse("auth.sms.max_frequency", () => parseGoDuration(smsForValidation.max_frequency), ); - const authEnabledForValidation = envOverrideBool( + const authEnabledForValidation = yield* envOverrideBool( "SUPABASE_AUTH_ENABLED", config.auth.enabled, "auth.enabled", projectEnvValues, - ); + ).pipe(Effect.orDie); if ( authEnabledForValidation && !smsForValidation.twilio.enabled && @@ -161,33 +163,42 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu !smsForValidation.messagebird.enabled && !smsForValidation.textlocal.enabled && !smsForValidation.vonage.enabled && - envOverrideBool( + (yield* envOverrideBool( "SUPABASE_AUTH_SMS_ENABLE_SIGNUP", config.auth.sms.enable_signup, "auth.sms.enable_signup", projectEnvValues, - ) + ).pipe(Effect.orDie)) ) { yield* output.raw("WARN: no SMS provider is enabled. Disabling phone login\n", "stderr"); } - const gotrueSessionsForValidation = resolveGotrueSessions(config.auth.sessions, projectEnvValues); + const gotrueSessionsForValidation = yield* resolveGotrueSessions( + config.auth.sessions, + projectEnvValues, + ); if (gotrueSessionsForValidation?.timebox !== undefined) { - yield* wrapDbConfigOverride("auth.sessions.timebox", () => + yield* wrapDbConfigParse("auth.sessions.timebox", () => parseGoDuration(gotrueSessionsForValidation.timebox!), ); } if (gotrueSessionsForValidation?.inactivity_timeout !== undefined) { - yield* wrapDbConfigOverride("auth.sessions.inactivity_timeout", () => + yield* wrapDbConfigParse("auth.sessions.inactivity_timeout", () => parseGoDuration(gotrueSessionsForValidation.inactivity_timeout!), ); } - yield* wrapDbConfigOverride("auth.mfa.phone.max_frequency", () => - parseGoDuration(resolveAuthMfa(config.auth.mfa, projectEnvValues).phone.max_frequency), + const mfaForValidation = yield* wrapDbConfigOverride( + "auth.mfa.phone.max_frequency", + resolveAuthMfa(config.auth.mfa, projectEnvValues), + ); + yield* wrapDbConfigParse("auth.mfa.phone.max_frequency", () => + parseGoDuration(mfaForValidation.phone.max_frequency), ); - yield* wrapDbConfigOverride("auth.rate_limit", () => + yield* wrapDbConfigOverride( + "auth.rate_limit", resolveGotrueRateLimit(config.auth.rate_limit, projectEnvValues), ); - yield* wrapDbConfigOverride("auth.jwt_expiry", () => + yield* wrapDbConfigOverride( + "auth.jwt_expiry", envOverrideUint( "SUPABASE_AUTH_JWT_EXPIRY", "auth.jwt_expiry", @@ -195,7 +206,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("auth.enable_signup", () => + yield* wrapDbConfigOverride( + "auth.enable_signup", envOverrideBool( "SUPABASE_AUTH_ENABLE_SIGNUP", config.auth.enable_signup, @@ -203,7 +215,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("auth.enable_anonymous_sign_ins", () => + yield* wrapDbConfigOverride( + "auth.enable_anonymous_sign_ins", envOverrideBool( "SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS", config.auth.enable_anonymous_sign_ins, @@ -211,7 +224,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("auth.enable_refresh_token_rotation", () => + yield* wrapDbConfigOverride( + "auth.enable_refresh_token_rotation", envOverrideBool( "SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION", config.auth.enable_refresh_token_rotation, @@ -219,7 +233,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("auth.refresh_token_reuse_interval", () => + yield* wrapDbConfigOverride( + "auth.refresh_token_reuse_interval", envOverrideUint( "SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL", "auth.refresh_token_reuse_interval", @@ -227,7 +242,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("auth.enable_manual_linking", () => + yield* wrapDbConfigOverride( + "auth.enable_manual_linking", envOverrideBool( "SUPABASE_AUTH_ENABLE_MANUAL_LINKING", config.auth.enable_manual_linking, @@ -235,7 +251,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("auth.minimum_password_length", () => + yield* wrapDbConfigOverride( + "auth.minimum_password_length", envOverrideUint( "SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH", "auth.minimum_password_length", @@ -243,33 +260,39 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("auth.password_requirements", () => + yield* wrapDbConfigOverride( + "auth.password_requirements", envOverrideAuthPasswordRequirements(config.auth.password_requirements, projectEnvValues), ); - yield* wrapDbConfigOverride("auth.web3", () => - resolveGotrueWeb3(config.auth.web3, projectEnvValues), - ); - yield* wrapDbConfigOverride("auth.oauth_server", () => + yield* wrapDbConfigOverride("auth.web3", resolveGotrueWeb3(config.auth.web3, projectEnvValues)); + yield* wrapDbConfigOverride( + "auth.oauth_server", resolveGotrueOAuthServer(config.auth.oauth_server, projectEnvValues), ); - yield* wrapDbConfigOverride("auth.passkey", () => + yield* wrapDbConfigOverride( + "auth.passkey", resolveGotruePasskeyWebauthn(loaded?.document, projectEnvValues), ); - yield* wrapDbConfigOverride("auth.external", () => + yield* wrapDbConfigOverride( + "auth.external", resolveAuthExternalProviders(authDocForValidation, config.auth.external, projectEnvValues), ); - yield* wrapDbConfigOverride("auth.third_party", () => + yield* wrapDbConfigOverride( + "auth.third_party", resolveThirdPartyProviders(config.auth.third_party, projectEnvValues), ); - yield* wrapDbConfigOverride("auth.hook", () => + yield* wrapDbConfigOverride( + "auth.hook", resolveAuthHooks(authDocForValidation, config.auth.hook, projectEnvValues), ); - yield* wrapDbConfigOverride("api.enabled", () => + yield* wrapDbConfigOverride( + "api.enabled", envOverrideBool("SUPABASE_API_ENABLED", config.api.enabled, "api.enabled", projectEnvValues), ); - yield* wrapDbConfigOverride("api.tls.enabled", () => + yield* wrapDbConfigOverride( + "api.tls.enabled", envOverrideBool( "SUPABASE_API_TLS_ENABLED", config.api.tls.enabled, @@ -277,14 +300,17 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("api.max_rows", () => + yield* wrapDbConfigOverride( + "api.max_rows", envOverrideApiMaxRows(config.api.max_rows, projectEnvValues), ); - yield* wrapDbConfigOverride("api.port", () => + yield* wrapDbConfigOverride( + "api.port", envOverridePort("SUPABASE_API_PORT", config.api.port, "api.port", projectEnvValues), ); - yield* wrapDbConfigOverride("storage.vector.enabled", () => + yield* wrapDbConfigOverride( + "storage.vector.enabled", envOverrideBool( "SUPABASE_STORAGE_VECTOR_ENABLED", config.storage.vector.enabled, @@ -292,7 +318,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.s3_protocol.enabled", () => + yield* wrapDbConfigOverride( + "storage.s3_protocol.enabled", envOverrideBool( "SUPABASE_STORAGE_S3_PROTOCOL_ENABLED", config.storage.s3_protocol.enabled, @@ -300,7 +327,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.analytics.enabled", () => + yield* wrapDbConfigOverride( + "storage.analytics.enabled", envOverrideBool( "SUPABASE_STORAGE_ANALYTICS_ENABLED", config.storage.analytics.enabled, @@ -308,7 +336,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.analytics.max_namespaces", () => + yield* wrapDbConfigOverride( + "storage.analytics.max_namespaces", envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_NAMESPACES", "storage.analytics.max_namespaces", @@ -316,7 +345,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.analytics.max_tables", () => + yield* wrapDbConfigOverride( + "storage.analytics.max_tables", envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_TABLES", "storage.analytics.max_tables", @@ -324,7 +354,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.analytics.max_catalogs", () => + yield* wrapDbConfigOverride( + "storage.analytics.max_catalogs", envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_CATALOGS", "storage.analytics.max_catalogs", @@ -332,7 +363,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.vector.max_buckets", () => + yield* wrapDbConfigOverride( + "storage.vector.max_buckets", envOverrideUint( "SUPABASE_STORAGE_VECTOR_MAX_BUCKETS", "storage.vector.max_buckets", @@ -340,7 +372,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.vector.max_indexes", () => + yield* wrapDbConfigOverride( + "storage.vector.max_indexes", envOverrideUint( "SUPABASE_STORAGE_VECTOR_MAX_INDEXES", "storage.vector.max_indexes", @@ -351,7 +384,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu const imageTransformationSectionPresent = asRecord(asRecord(loaded?.document?.["storage"])?.["image_transformation"]) !== undefined; if (imageTransformationSectionPresent) { - yield* wrapDbConfigOverride("storage.image_transformation.enabled", () => + yield* wrapDbConfigOverride( + "storage.image_transformation.enabled", envOverrideBool( "SUPABASE_STORAGE_IMAGE_TRANSFORMATION_ENABLED", config.storage.image_transformation?.enabled ?? false, @@ -361,7 +395,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu ); } - const localSmtpPortForValidation = yield* wrapDbConfigOverride("local_smtp.port", () => + const localSmtpPortForValidation = yield* wrapDbConfigOverride( + "local_smtp.port", envOverridePort( "SUPABASE_LOCAL_SMTP_PORT", config.local_smtp.port, @@ -369,7 +404,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("local_smtp.smtp_port", () => + yield* wrapDbConfigOverride( + "local_smtp.smtp_port", envOverridePort( "SUPABASE_LOCAL_SMTP_SMTP_PORT", config.local_smtp.smtp_port ?? 0, @@ -377,7 +413,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("local_smtp.pop3_port", () => + yield* wrapDbConfigOverride( + "local_smtp.pop3_port", envOverridePort( "SUPABASE_LOCAL_SMTP_POP3_PORT", config.local_smtp.pop3_port ?? 0, @@ -385,7 +422,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("analytics.port", () => + yield* wrapDbConfigOverride( + "analytics.port", envOverridePort( "SUPABASE_ANALYTICS_PORT", config.analytics.port, @@ -393,7 +431,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("analytics.vector_port", () => + yield* wrapDbConfigOverride( + "analytics.vector_port", envOverridePort( "SUPABASE_ANALYTICS_VECTOR_PORT", config.analytics.vector_port ?? 0, @@ -402,7 +441,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu ), ); - yield* wrapDbConfigOverride("db.pooler.enabled", () => + yield* wrapDbConfigOverride( + "db.pooler.enabled", envOverrideBool( "SUPABASE_DB_POOLER_ENABLED", config.db.pooler.enabled, @@ -410,7 +450,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("db.pooler.port", () => + yield* wrapDbConfigOverride( + "db.pooler.port", envOverridePort( "SUPABASE_DB_POOLER_PORT", config.db.pooler.port, @@ -418,20 +459,25 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("db.pooler.pool_mode", () => + yield* wrapDbConfigOverride( + "db.pooler.pool_mode", envOverridePoolMode(config.db.pooler.pool_mode, projectEnvValues), ); - yield* wrapDbConfigOverride("db.pooler.default_pool_size", () => + yield* wrapDbConfigOverride( + "db.pooler.default_pool_size", envOverrideDefaultPoolSize(config.db.pooler.default_pool_size, projectEnvValues), ); - yield* wrapDbConfigOverride("db.pooler.max_client_conn", () => + yield* wrapDbConfigOverride( + "db.pooler.max_client_conn", envOverrideMaxClientConn(config.db.pooler.max_client_conn, projectEnvValues), ); - yield* wrapDbConfigOverride("edge_runtime.policy", () => + yield* wrapDbConfigOverride( + "edge_runtime.policy", envOverrideEdgeRuntimePolicy(config.edge_runtime.policy, projectEnvValues), ); - yield* wrapDbConfigOverride("edge_runtime.inspector_port", () => + yield* wrapDbConfigOverride( + "edge_runtime.inspector_port", envOverridePort( "SUPABASE_EDGE_RUNTIME_INSPECTOR_PORT", config.edge_runtime.inspector_port, @@ -440,18 +486,22 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu ), ); - yield* wrapDbConfigOverride("realtime.ip_version", () => + yield* wrapDbConfigOverride( + "realtime.ip_version", envOverrideRealtimeIpVersion(config.realtime.ip_version, projectEnvValues), ); - yield* wrapDbConfigOverride("realtime.max_header_length", () => + yield* wrapDbConfigOverride( + "realtime.max_header_length", envOverrideRealtimeMaxHeaderLength(config.realtime.max_header_length, projectEnvValues), ); - yield* wrapDbConfigOverride("db.settings", () => + yield* wrapDbConfigOverride( + "db.settings", resolveDbSettingsEnvOverrides(config.db.settings, projectEnvValues), ); - yield* wrapDbConfigOverride("realtime.enabled", () => + yield* wrapDbConfigOverride( + "realtime.enabled", envOverrideBool( "SUPABASE_REALTIME_ENABLED", config.realtime.enabled, @@ -459,7 +509,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.enabled", () => + yield* wrapDbConfigOverride( + "storage.enabled", envOverrideBool( "SUPABASE_STORAGE_ENABLED", config.storage.enabled, @@ -467,23 +518,27 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("storage.file_size_limit", () => - ramInBytes( - envOverride( - "SUPABASE_STORAGE_FILE_SIZE_LIMIT", - config.storage.file_size_limit, - projectEnvValues, - ) ?? config.storage.file_size_limit, - ), - ); - yield* wrapDbConfigOverride("db.health_timeout", () => - resolveHealthTimeoutSeconds( - envOverride("SUPABASE_DB_HEALTH_TIMEOUT", config.db.health_timeout, projectEnvValues) ?? - config.db.health_timeout, - ), + const storageFileSizeLimitForValidation = + (yield* envOverride( + "SUPABASE_STORAGE_FILE_SIZE_LIMIT", + config.storage.file_size_limit, + projectEnvValues, + )) ?? config.storage.file_size_limit; + yield* wrapDbConfigParse("storage.file_size_limit", () => + ramInBytes(storageFileSizeLimitForValidation), + ); + const dbHealthTimeoutForValidation = + (yield* envOverride( + "SUPABASE_DB_HEALTH_TIMEOUT", + config.db.health_timeout, + projectEnvValues, + )) ?? config.db.health_timeout; + yield* wrapDbConfigParse("db.health_timeout", () => + resolveHealthTimeoutSeconds(dbHealthTimeoutForValidation), ); - yield* wrapDbConfigOverride("edge_runtime.enabled", () => + yield* wrapDbConfigOverride( + "edge_runtime.enabled", envOverrideBool( "SUPABASE_EDGE_RUNTIME_ENABLED", config.edge_runtime.enabled, @@ -491,7 +546,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - yield* wrapDbConfigOverride("db.network_restrictions.enabled", () => + yield* wrapDbConfigOverride( + "db.network_restrictions.enabled", envOverrideBool( "SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED", config.db.network_restrictions.enabled, @@ -502,7 +558,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu const sslEnforcementSectionPresent = asRecord(asRecord(loaded?.document?.["db"])?.["ssl_enforcement"]) !== undefined; if (sslEnforcementSectionPresent) { - yield* wrapDbConfigOverride("db.ssl_enforcement.enabled", () => + yield* wrapDbConfigOverride( + "db.ssl_enforcement.enabled", envOverrideBool( "SUPABASE_DB_SSL_ENFORCEMENT_ENABLED", config.db.ssl_enforcement?.enabled ?? false, @@ -511,7 +568,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu ), ); } - const studioEnabledForValidation = yield* wrapDbConfigOverride("studio.enabled", () => + const studioEnabledForValidation = yield* wrapDbConfigOverride( + "studio.enabled", envOverrideBool( "SUPABASE_STUDIO_ENABLED", config.studio.enabled, @@ -519,7 +577,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu projectEnvValues, ), ); - const studioPortForValidation = yield* wrapDbConfigOverride("studio.port", () => + const studioPortForValidation = yield* wrapDbConfigOverride( + "studio.port", envOverridePort("SUPABASE_STUDIO_PORT", config.studio.port, "studio.port", projectEnvValues), ); if (studioEnabledForValidation && studioPortForValidation === 0) { @@ -528,7 +587,7 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu }); } const studioApiUrlForValidation = - envOverride("SUPABASE_STUDIO_API_URL", config.studio.api_url, projectEnvValues) ?? + (yield* envOverride("SUPABASE_STUDIO_API_URL", config.studio.api_url, projectEnvValues)) ?? config.studio.api_url; if (studioEnabledForValidation) { yield* Effect.try({ @@ -539,7 +598,8 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu }), }); } - const localSmtpEnabledForValidation = yield* wrapDbConfigOverride("local_smtp.enabled", () => + const localSmtpEnabledForValidation = yield* wrapDbConfigOverride( + "local_smtp.enabled", envOverrideBool( "SUPABASE_LOCAL_SMTP_ENABLED", config.local_smtp.enabled, @@ -555,24 +615,24 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu // `resolveLocalConfigValues` is the same resolver `buildLocalDbContainerInputs` calls again // below to build the real `values`. Calling it eagerly here too forces its internal - // decode-time throws (auth.captcha, jwt_secret length, signing_keys_path, api.tls cert/key + // decode-time failures (auth.captcha, jwt_secret length, signing_keys_path, api.tls cert/key // reads, auth.external required fields, email/notification template reads) to surface before // the already-running shortcut. The result is discarded; `buildLocalDbContainerInputs` below // re-resolves the real values. - yield* Effect.try({ - try: () => - resolveLocalConfigValues( - config, - hostnameForValidation, - cliSettings.workdir, - projectEnvValues, - loaded?.document, - ), - catch: (cause) => - new DbConfigLoadError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); + yield* resolveLocalConfigValues( + config, + hostnameForValidation, + cliSettings.workdir, + projectEnvValues, + loaded?.document, + ).pipe( + Effect.mapError( + (cause) => + new DbConfigLoadError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + ), + ); // If the db container is already up, tell the caller and stop here. Runs after the config // load/validation above. diff --git a/apps/cli/src/command-internal/db-config.toml-read.ts b/apps/cli/src/command-internal/db-config.toml-read.ts index 5134fbd1b2..bb1436db72 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.ts @@ -1,15 +1,17 @@ import { normalizeDeprecatedOrioleDBVersion } from "@supabase/config/internal"; -import { Config, Effect, Match, type FileSystem, Option, type Path } from "effect"; +import { Config, Effect, Match, FileSystem, Option, Path } from "effect"; import * as SmolToml from "smol-toml"; import { PROJECT_REF_PATTERN, type AnalyticsInput, type AuthInput, type CaptchaInput, + ConfigValidateError, type ConfigValidationInput, type DbInput, emailContentPathReadErrorMessage, type ExperimentalInput, + fsErrorCause, type HookInput, type MfaFactorInput, parseGoBool, @@ -1093,7 +1095,9 @@ export const envOverrideLookup = Effect.fnUntraced(function* ( export const warnDeprecatedOrioleDBVersionEnv = Effect.fnUntraced(function* ( envLookup: DbTomlValues["envLookup"], ) { - const warning = yield* deprecatedOrioleDBVersionEnvWarning(envLookup); + const warning = yield* deprecatedOrioleDBVersionEnvWarning((name) => + Effect.succeed(envLookup(name)), + ); if (warning !== undefined) yield* (yield* Output).raw(`${warning}\n`, "stderr"); }); @@ -1328,7 +1332,9 @@ const readDbTomlCore = Effect.fnUntraced(function* ( const expandString = (value: unknown): Option.Option => typeof value === "string" ? nonEmptyString(expandEnv(value, lookup)) : Option.none(); const orioledbVersionRaw = - (remoteWins("db.orioledb_version") ? undefined : readOrioleDBVersionEnvOverride(envOverride)) ?? + (remoteWins("db.orioledb_version") + ? undefined + : yield* readOrioleDBVersionEnvOverride((name) => Effect.succeed(envOverride(name)))) ?? db?.["orioledb_version"]; const orioledbVersion = expandString(orioledbVersionRaw); // The defaults-only fallback says nothing about the project's engine. @@ -1569,7 +1575,7 @@ const readDbTomlCore = Effect.fnUntraced(function* ( const signingKeysPath = str(authRawResolved, "signing_keys_path"); if (signingKeysPath.length > 0) { const keysJson = yield* fs - .readFileString(resolveSigningKeysPath(workdir, signingKeysPath)) + .readFileString(resolveSigningKeysPath(path, workdir, signingKeysPath)) .pipe( Effect.mapError( (cause) => new DbConfigLoadError({ message: signingKeysReadErrorMessage(cause) }), @@ -1647,20 +1653,22 @@ const readDbTomlCore = Effect.fnUntraced(function* ( for (const name of Object.keys(templatesRaw)) { const tmpl = asRecord(templatesRaw[name]); if (tmpl === undefined) continue; - const contentPath = yield* Effect.try({ - try: () => - resolveEmailTemplateContentPath({ - section: "template", - name, - contentPath: str(tmpl, "content_path"), - contentPresent: tmpl["content"] !== undefined, - base: workdir, - }), - catch: (cause) => - new DbConfigLoadError({ + const contentPath = yield* resolveEmailTemplateContentPath({ + section: "template", + name, + contentPath: str(tmpl, "content_path"), + contentPresent: tmpl["content"] !== undefined, + base: workdir, + }).pipe( + Effect.provideService(FileSystem.FileSystem, fs), + Effect.provideService(Path.Path, path), + Effect.mapError((error) => { + const cause = error instanceof ConfigValidateError ? error : fsErrorCause(error); + return new DbConfigLoadError({ message: cause instanceof Error ? cause.message : String(cause), - }), - }); + }); + }), + ); if (contentPath === undefined) continue; yield* fs.readFileString(contentPath).pipe( Effect.mapError( @@ -1682,20 +1690,22 @@ const readDbTomlCore = Effect.fnUntraced(function* ( ) { continue; } - const contentPath = yield* Effect.try({ - try: () => - resolveEmailTemplateContentPath({ - section: "notification", - name, - contentPath: str(tmpl, "content_path"), - contentPresent: tmpl["content"] !== undefined, - base: workdir, - }), - catch: (cause) => - new DbConfigLoadError({ + const contentPath = yield* resolveEmailTemplateContentPath({ + section: "notification", + name, + contentPath: str(tmpl, "content_path"), + contentPresent: tmpl["content"] !== undefined, + base: workdir, + }).pipe( + Effect.provideService(FileSystem.FileSystem, fs), + Effect.provideService(Path.Path, path), + Effect.mapError((error) => { + const cause = error instanceof ConfigValidateError ? error : fsErrorCause(error); + return new DbConfigLoadError({ message: cause instanceof Error ? cause.message : String(cause), - }), - }); + }); + }), + ); if (contentPath === undefined) continue; yield* fs.readFileString(contentPath).pipe( Effect.mapError( diff --git a/apps/cli/src/command-internal/functions-go-config.ts b/apps/cli/src/command-internal/functions-go-config.ts index d107110529..e0f053b1bc 100644 --- a/apps/cli/src/command-internal/functions-go-config.ts +++ b/apps/cli/src/command-internal/functions-go-config.ts @@ -15,31 +15,27 @@ function toError(cause: unknown): Error { * Delegates to the same `loadLocalProjectContext`/`resolveLocalConfigValues` pair * `start`/`stop`/`status` already use, rather than re-implementing either. Their derived * local-dev values (JWTs, URLs) are discarded here; only `projectId`/`edgeRuntimeDenoVersion` and - * the validation side effect (throws on the first failure) matter to these three commands. + * the validation side effect (fails on the first error) matter to these three commands. */ export const functionsGoConfigCompat: FunctionsGoConfigCompat = { load: ({ projectRoot, projectRef }) => Effect.gen(function* () { const context = yield* loadLocalProjectContext(projectRoot, toError, projectRef); - const validated = yield* Effect.try({ - try: () => - resolveLocalConfigValues( - context.config, - context.hostname, - projectRoot, - context.projectEnvValues, - context.loaded?.document, - // No `[remotes.]` override-tier gating (empty set, the default): the remote - // block already merged over the base config at file level via - // `loadLocalProjectContext`'s `projectRef` threading above. Known narrow divergence: - // an ambient `SUPABASE_EDGE_RUNTIME_DENO_VERSION` still beats a matched remote - // block's own `deno_version` here, since computing the override keys needs - // `db-config.toml-read.ts`'s remote-resolution pipeline, which this path doesn't run. - undefined, - projectRef, - ), - catch: toError, - }); + const validated = yield* resolveLocalConfigValues( + context.config, + context.hostname, + projectRoot, + context.projectEnvValues, + context.loaded?.document, + // No `[remotes.]` override-tier gating (empty set, the default): the remote + // block already merged over the base config at file level via + // `loadLocalProjectContext`'s `projectRef` threading above. Known narrow divergence: + // an ambient `SUPABASE_EDGE_RUNTIME_DENO_VERSION` still beats a matched remote + // block's own `deno_version` here, since computing the override keys needs + // `db-config.toml-read.ts`'s remote-resolution pipeline, which this path doesn't run. + undefined, + projectRef, + ); return { loaded: context.loaded, projectEnvValues: context.projectEnvValues, diff --git a/apps/cli/src/command-internal/local-config-values.ts b/apps/cli/src/command-internal/local-config-values.ts index 8240608de6..678ddb7345 100644 --- a/apps/cli/src/command-internal/local-config-values.ts +++ b/apps/cli/src/command-internal/local-config-values.ts @@ -1,6 +1,3 @@ -import { readFileSync } from "node:fs"; -import { basename } from "node:path"; - import type { CliConfig } from "@supabase/config"; import { ENV_CAPTURE_REGEX } from "@supabase/config/internal"; import { @@ -15,7 +12,7 @@ import { defaultPublishableKey, defaultSecretKey, } from "../shared/stack-constants.ts"; -import { Context, Effect, Option, Ref, Schema } from "effect"; +import { Context, Data, Effect, FileSystem, Option, Path, Ref, Schema } from "effect"; import * as Base64Url from "effect/encoding/Base64Url"; import { @@ -46,6 +43,7 @@ import { type DbInput, emailContentPathReadErrorMessage, type ExperimentalInput, + fsErrorCause, type HookInput, type LocalSmtpInput, type MfaFactorInput, @@ -63,6 +61,7 @@ import { } from "./config-validate.ts"; import { DEFAULT_SIGNING_KEY, generateAsymmetricGoJwt, generateGoJwt, type Jwk } from "./go-jwt.ts"; import { collectDotenvPrivateKeys, decryptSecret, isEncryptedSecret } from "./vault-decrypt.ts"; +import { shellEnv } from "./viper-env.ts"; /** * Resolves local-dev config values (URLs, ports, keys) for `status`/`stop`, filling in @@ -147,13 +146,13 @@ function apiUrlWithPath(apiExternalUrl: string, path: string): string { } /** - * Thrown by {@link resolveLocalConfigValues} when `auth.jwt_secret` is configured but too + * Raised by {@link resolveLocalConfigValues} when `auth.jwt_secret` is configured but too * short to sign with. Validated at config-load time, before any command renders output. */ -export class InvalidJwtSecretError extends Error { +export class InvalidJwtSecretError extends Data.Error<{ readonly message: string }> { static readonly [ErrorActionabilityFingerprintId] = "InvalidJwtSecretError"; constructor() { - super("Invalid config for auth.jwt_secret. Must be at least 16 characters"); + super({ message: "Invalid config for auth.jwt_secret. Must be at least 16 characters" }); this.name = "InvalidJwtSecretError"; } get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { @@ -165,14 +164,31 @@ export class InvalidJwtSecretError extends Error { const MIN_JWT_SECRET_LENGTH = 16; /** - * Thrown by {@link envOverridePort} when a `SUPABASE_*_PORT` env/dotenv override doesn't + * A failure that is not one of this module's config errors (a malformed uint override, a + * signing-key error), keeping the original error's name and message and no actionability. + */ +class LocalConfigValuesError extends Data.Error<{ readonly message: string }> { + constructor(message: string, name = "Error") { + super({ message }); + /* Keeps the wrapped error's name (as a non-enumerable own property) and message. */ + if (name !== "Error") Reflect.defineProperty(this, "name", { value: name, writable: true }); + } +} + +const thrownError = (cause: unknown) => + cause instanceof Error + ? new LocalConfigValuesError(cause.message, cause.name) + : new LocalConfigValuesError(String(cause)); + +/** + * Raised by {@link envOverridePort} when a `SUPABASE_*_PORT` env/dotenv override doesn't * parse as a valid port. A malformed port override always hard-fails config loading; there's * no path that reaches `status`/`stop` with one. */ -export class InvalidPortEnvOverrideError extends Error { +export class InvalidPortEnvOverrideError extends Data.Error<{ readonly message: string }> { static readonly [ErrorActionabilityFingerprintId] = "InvalidPortEnvOverrideError"; constructor(dottedFieldPath: string, value: string) { - super(`Invalid config for ${dottedFieldPath}: cannot parse "${value}" as a port`); + super({ message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as a port` }); this.name = "InvalidPortEnvOverrideError"; } get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { @@ -186,63 +202,61 @@ const MAX_PORT = 65535; /** * Port-flavored sibling of {@link envOverride}/{@link envOverrideBool}. Unlike the boolean * sibling, which falls back to the configured value on a malformed override, a bad port - * override throws {@link InvalidPortEnvOverrideError} instead. + * override fails with {@link InvalidPortEnvOverrideError} instead. */ -export function envOverridePort( +export const envOverridePort = Effect.fnUntraced(function* ( name: string, configuredPort: number, dottedFieldPath: string, projectEnvValues: Readonly> | undefined, -): number { - const value = envOverride(name, undefined, projectEnvValues); +) { + const value = yield* envOverride(name, undefined, projectEnvValues); if (value === undefined) return configuredPort; const parsed = parseGoBaseZeroUint(value); if (parsed === undefined || parsed > BigInt(MAX_PORT)) { - throw new InvalidPortEnvOverrideError(dottedFieldPath, value); + return yield* new InvalidPortEnvOverrideError(dottedFieldPath, value); } return Number(parsed); -} +}); + +/** `projectEnvValues` first, then the ambient environment. */ +const lookupEnv = Effect.fnUntraced(function* ( + name: string, + projectEnvValues: Readonly> | undefined, +) { + return projectEnvValues?.[name] ?? (yield* shellEnv(name)); +}); /** * Resolves a `SUPABASE_` override for a config field: checks `projectEnvValues` - * (the project's already-resolved dotenv values) before falling back to `process.env`, - * treating an empty value as unset. + * (the project's already-resolved dotenv values) before falling back to the ambient `Config` + * environment, treating an empty value as unset. * * The resolved value can itself be an `env(VAR)` indirection (e.g. `env(API_ENABLED)`), * resolved with the same precedence; an unresolved indirection is returned as-is. */ -export function envOverride( - name: string, - configured: string, - projectEnvValues: Readonly> | undefined, -): string; -export function envOverride( - name: string, - configured: string | undefined, - projectEnvValues: Readonly> | undefined, -): string | undefined; -export function envOverride( +export const envOverride = Effect.fnUntraced(function* ( name: string, - configured: string | undefined, + configured: Configured, projectEnvValues: Readonly> | undefined, -): string | undefined { - const value = projectEnvValues?.[name] ?? process.env[name]; +) { + const value = yield* lookupEnv(name, projectEnvValues); if (value === undefined || value.length === 0) return configured; const indirection = ENV_CAPTURE_REGEX.exec(value)?.[1]; if (indirection === undefined) return value; - const resolved = projectEnvValues?.[indirection] ?? process.env[indirection]; + const resolved = yield* lookupEnv(indirection, projectEnvValues); return resolved !== undefined && resolved.length > 0 ? resolved : value; -} +}); /** - * Thrown by {@link envOverrideBool} when a `SUPABASE_*_ENABLED` (or other bool-typed) + * Raised by {@link envOverrideBool} when a `SUPABASE_*_ENABLED` (or other bool-typed) * env/dotenv override doesn't parse as an accepted bool spelling. A malformed bool override * always hard-fails config loading, same as {@link InvalidPortEnvOverrideError}. */ -export class InvalidBoolEnvOverrideError extends Error { +export class InvalidBoolEnvOverrideError extends Data.Error<{ readonly message: string }> { static readonly [ErrorActionabilityFingerprintId] = "InvalidBoolEnvOverrideError"; constructor(dottedFieldPath: string, value: string) { - super(`Invalid config for ${dottedFieldPath}: cannot parse "${value}" as a bool`); + super({ message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as a bool` }); this.name = "InvalidBoolEnvOverrideError"; } get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { @@ -253,31 +267,33 @@ export class InvalidBoolEnvOverrideError extends Error { /** * Boolean-flavored sibling of {@link envOverride} for `SUPABASE_*` fields decoded as a * native bool (`api.tls.enabled`, `auth.enabled`, and other `
.enabled` gates). A - * malformed override throws {@link InvalidBoolEnvOverrideError} rather than falling back + * malformed override fails with {@link InvalidBoolEnvOverrideError} rather than falling back * to `configured`. */ -export function envOverrideBool( +export const envOverrideBool = Effect.fnUntraced(function* ( name: string, configured: boolean, dottedFieldPath: string, projectEnvValues: Readonly> | undefined, -): boolean { - const value = envOverride(name, undefined, projectEnvValues); +) { + const value = yield* envOverride(name, undefined, projectEnvValues); if (value === undefined) return configured; const parsed = parseGoBool(value); if (parsed === undefined) { - throw new InvalidBoolEnvOverrideError(dottedFieldPath, value); + return yield* new InvalidBoolEnvOverrideError(dottedFieldPath, value); } return parsed; -} +}); -/** Thrown by {@link envOverrideAnalyticsBackend} when `SUPABASE_ANALYTICS_BACKEND` isn't `"postgres"` or `"bigquery"`. */ -export class InvalidAnalyticsBackendEnvOverrideError extends Error { +/** Raised by {@link envOverrideAnalyticsBackend} when `SUPABASE_ANALYTICS_BACKEND` isn't `"postgres"` or `"bigquery"`. */ +export class InvalidAnalyticsBackendEnvOverrideError extends Data.Error<{ + readonly message: string; +}> { static readonly [ErrorActionabilityFingerprintId] = "InvalidAnalyticsBackendEnvOverrideError"; constructor(dottedFieldPath: string, value: string) { - super( - `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "postgres", "bigquery"`, - ); + super({ + message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "postgres", "bigquery"`, + }); this.name = "InvalidAnalyticsBackendEnvOverrideError"; } get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { @@ -291,27 +307,30 @@ export class InvalidAnalyticsBackendEnvOverrideError extends Error { * `skipEnvOverride` lets a matched remote-config value win over a conflicting * `SUPABASE_ANALYTICS_BACKEND`. */ -export function envOverrideAnalyticsBackend( +export const envOverrideAnalyticsBackend = Effect.fnUntraced(function* ( configured: string, projectEnvValues: Readonly> | undefined, skipEnvOverride = false, -): "postgres" | "bigquery" { +) { const value = skipEnvOverride ? configured - : (envOverride("SUPABASE_ANALYTICS_BACKEND", undefined, projectEnvValues) ?? configured); + : ((yield* envOverride("SUPABASE_ANALYTICS_BACKEND", undefined, projectEnvValues)) ?? + configured); if (value !== "postgres" && value !== "bigquery") { - throw new InvalidAnalyticsBackendEnvOverrideError("analytics.backend", value); + return yield* new InvalidAnalyticsBackendEnvOverrideError("analytics.backend", value); } return value; -} +}); -/** Thrown by {@link envOverrideRealtimeIpVersion} when `SUPABASE_REALTIME_IP_VERSION` isn't `"IPv4"` or `"IPv6"`. */ -export class InvalidRealtimeIpVersionEnvOverrideError extends Error { +/** Raised by {@link envOverrideRealtimeIpVersion} when `SUPABASE_REALTIME_IP_VERSION` isn't `"IPv4"` or `"IPv6"`. */ +export class InvalidRealtimeIpVersionEnvOverrideError extends Data.Error<{ + readonly message: string; +}> { static readonly [ErrorActionabilityFingerprintId] = "InvalidRealtimeIpVersionEnvOverrideError"; constructor(dottedFieldPath: string, value: string) { - super( - `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "IPv4", "IPv6"`, - ); + super({ + message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "IPv4", "IPv6"`, + }); this.name = "InvalidRealtimeIpVersionEnvOverrideError"; } @@ -320,46 +339,51 @@ export class InvalidRealtimeIpVersionEnvOverrideError extends Error { } } -export function envOverrideRealtimeIpVersion( +export const envOverrideRealtimeIpVersion = Effect.fnUntraced(function* ( configured: string, projectEnvValues: Readonly> | undefined, -): "IPv4" | "IPv6" { +) { const value = - envOverride("SUPABASE_REALTIME_IP_VERSION", undefined, projectEnvValues) ?? configured; + (yield* envOverride("SUPABASE_REALTIME_IP_VERSION", undefined, projectEnvValues)) ?? configured; if (value !== "IPv4" && value !== "IPv6") { - throw new InvalidRealtimeIpVersionEnvOverrideError("realtime.ip_version", value); + return yield* new InvalidRealtimeIpVersionEnvOverrideError("realtime.ip_version", value); } return value; -} +}); /** `SUPABASE_REALTIME_MAX_HEADER_LENGTH` — see {@link envOverrideUint}. */ -export function envOverrideRealtimeMaxHeaderLength( +export const envOverrideRealtimeMaxHeaderLength = Effect.fnUntraced(function* ( configured: number, projectEnvValues: Readonly> | undefined, -): number { - return envOverrideUint( +) { + return yield* envOverrideUint( "SUPABASE_REALTIME_MAX_HEADER_LENGTH", "realtime.max_header_length", configured, projectEnvValues, ); -} +}); /** `SUPABASE_API_MAX_ROWS` — see {@link envOverrideUint}. */ -export function envOverrideApiMaxRows( +export const envOverrideApiMaxRows = Effect.fnUntraced(function* ( configured: number, projectEnvValues: Readonly> | undefined, -): number { - return envOverrideUint("SUPABASE_API_MAX_ROWS", "api.max_rows", configured, projectEnvValues); -} +) { + return yield* envOverrideUint( + "SUPABASE_API_MAX_ROWS", + "api.max_rows", + configured, + projectEnvValues, + ); +}); -/** Thrown by {@link envOverridePoolMode} when `SUPABASE_DB_POOLER_POOL_MODE` isn't `"transaction"` or `"session"`. */ -export class InvalidPoolModeEnvOverrideError extends Error { +/** Raised by {@link envOverridePoolMode} when `SUPABASE_DB_POOLER_POOL_MODE` isn't `"transaction"` or `"session"`. */ +export class InvalidPoolModeEnvOverrideError extends Data.Error<{ readonly message: string }> { static readonly [ErrorActionabilityFingerprintId] = "InvalidPoolModeEnvOverrideError"; constructor(dottedFieldPath: string, value: string) { - super( - `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "transaction", "session"`, - ); + super({ + message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "transaction", "session"`, + }); this.name = "InvalidPoolModeEnvOverrideError"; } @@ -368,25 +392,27 @@ export class InvalidPoolModeEnvOverrideError extends Error { } } -export function envOverridePoolMode( +export const envOverridePoolMode = Effect.fnUntraced(function* ( configured: string, projectEnvValues: Readonly> | undefined, -): "transaction" | "session" { +) { const value = - envOverride("SUPABASE_DB_POOLER_POOL_MODE", undefined, projectEnvValues) ?? configured; + (yield* envOverride("SUPABASE_DB_POOLER_POOL_MODE", undefined, projectEnvValues)) ?? configured; if (value !== "transaction" && value !== "session") { - throw new InvalidPoolModeEnvOverrideError("db.pooler.pool_mode", value); + return yield* new InvalidPoolModeEnvOverrideError("db.pooler.pool_mode", value); } return value; -} +}); -/** Thrown by {@link envOverrideEdgeRuntimePolicy} when `SUPABASE_EDGE_RUNTIME_POLICY` isn't `"per_worker"` or `"oneshot"`. */ -export class InvalidEdgeRuntimePolicyEnvOverrideError extends Error { +/** Raised by {@link envOverrideEdgeRuntimePolicy} when `SUPABASE_EDGE_RUNTIME_POLICY` isn't `"per_worker"` or `"oneshot"`. */ +export class InvalidEdgeRuntimePolicyEnvOverrideError extends Data.Error<{ + readonly message: string; +}> { static readonly [ErrorActionabilityFingerprintId] = "InvalidEdgeRuntimePolicyEnvOverrideError"; constructor(dottedFieldPath: string, value: string) { - super( - `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "per_worker", "oneshot"`, - ); + super({ + message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "per_worker", "oneshot"`, + }); this.name = "InvalidEdgeRuntimePolicyEnvOverrideError"; } @@ -395,43 +421,43 @@ export class InvalidEdgeRuntimePolicyEnvOverrideError extends Error { } } -export function envOverrideEdgeRuntimePolicy( +export const envOverrideEdgeRuntimePolicy = Effect.fnUntraced(function* ( configured: string, projectEnvValues: Readonly> | undefined, -): "per_worker" | "oneshot" { +) { const value = - envOverride("SUPABASE_EDGE_RUNTIME_POLICY", undefined, projectEnvValues) ?? configured; + (yield* envOverride("SUPABASE_EDGE_RUNTIME_POLICY", undefined, projectEnvValues)) ?? configured; if (value !== "per_worker" && value !== "oneshot") { - throw new InvalidEdgeRuntimePolicyEnvOverrideError("edge_runtime.policy", value); + return yield* new InvalidEdgeRuntimePolicyEnvOverrideError("edge_runtime.policy", value); } return value; -} +}); /** `SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE` — see {@link envOverrideUint}. */ -export function envOverrideDefaultPoolSize( +export const envOverrideDefaultPoolSize = Effect.fnUntraced(function* ( configured: number, projectEnvValues: Readonly> | undefined, -): number { - return envOverrideUint( +) { + return yield* envOverrideUint( "SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE", "db.pooler.default_pool_size", configured, projectEnvValues, ); -} +}); /** `SUPABASE_DB_POOLER_MAX_CLIENT_CONN` — see {@link envOverrideUint}. */ -export function envOverrideMaxClientConn( +export const envOverrideMaxClientConn = Effect.fnUntraced(function* ( configured: number, projectEnvValues: Readonly> | undefined, -): number { - return envOverrideUint( +) { + return yield* envOverrideUint( "SUPABASE_DB_POOLER_MAX_CLIENT_CONN", "db.pooler.max_client_conn", configured, projectEnvValues, ); -} +}); /** * Decrypts a resolved auth identity-key field when it's a dotenvx `encrypted:` value; an @@ -439,18 +465,18 @@ export function envOverrideMaxClientConn( * Applied after {@link envOverride} so an env-sourced override is decrypted too, not just * the config.toml value. */ -export function decryptAuthSecret( +export const decryptAuthSecret = Effect.fnUntraced(function* ( value: string | undefined, projectEnvValues: Readonly> | undefined, -): string | undefined { +) { if (value === undefined || !isEncryptedSecret(value)) return value; const dotenvPrivateKeys = collectDotenvPrivateKeys({ ...projectEnvValues, ...process.env }); const decrypted = decryptSecret(value, dotenvPrivateKeys); if (!decrypted.ok) { - throw new ConfigValidateError(`failed to parse config: ${decrypted.error}`); + return yield* new ConfigValidateError(`failed to parse config: ${decrypted.error}`); } return decrypted.value; -} +}); /** * Resolves `[auth.email.smtp]`'s full field set, including a presence-based `enabled` @@ -458,17 +484,17 @@ export function decryptAuthSecret( * when the key is absent from a present table). Exported so `start.handler.ts`'s GoTrue env * resolution reuses this instead of re-deriving it. */ -export function resolveAuthEmailSmtp( +export const resolveAuthEmailSmtp = Effect.fnUntraced(function* ( authDocument: Readonly> | undefined, projectEnvValues: Readonly> | undefined, /** - * Fields whose `envOverride`/`envOverrideBool`/`envOverridePort` calls below can throw on + * Fields whose `envOverride`/`envOverrideBool`/`envOverridePort` calls below can fail on * a malformed value (directly, or via {@link decryptAuthSecret}) belong in this set so a * matched remote block's value wins instead of aborting on an unrelated bad env var. * Defaults to empty for callers with no `[remotes.]` block to match against. */ remoteOverrideKeys: ReadonlySet = new Set(), -): (SmtpInput & { readonly senderName: string | undefined }) | undefined { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); const smtpDoc = asRecord(asRecord(authDocument?.["email"])?.["smtp"]); if (smtpDoc === undefined) return undefined; @@ -477,7 +503,7 @@ export function resolveAuthEmailSmtp( ? smtpDoc["enabled"] === undefined ? true : smtpDoc["enabled"] === true - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_EMAIL_SMTP_ENABLED", smtpDoc["enabled"] === undefined ? true : smtpDoc["enabled"] === true, "auth.email.smtp.enabled", @@ -487,16 +513,16 @@ export function resolveAuthEmailSmtp( ? typeof smtpDoc["host"] === "string" ? smtpDoc["host"] : "" - : (envOverride( + : ((yield* envOverride( "SUPABASE_AUTH_EMAIL_SMTP_HOST", typeof smtpDoc["host"] === "string" ? smtpDoc["host"] : "", projectEnvValues, - ) ?? ""), + )) ?? ""), port: remoteWins("auth.email.smtp.port") ? typeof smtpDoc["port"] === "number" ? smtpDoc["port"] : 0 - : envOverridePort( + : yield* envOverridePort( "SUPABASE_AUTH_EMAIL_SMTP_PORT", typeof smtpDoc["port"] === "number" ? smtpDoc["port"] : 0, "auth.email.smtp.port", @@ -506,46 +532,46 @@ export function resolveAuthEmailSmtp( ? typeof smtpDoc["user"] === "string" ? smtpDoc["user"] : "" - : (envOverride( + : ((yield* envOverride( "SUPABASE_AUTH_EMAIL_SMTP_USER", typeof smtpDoc["user"] === "string" ? smtpDoc["user"] : "", projectEnvValues, - ) ?? ""), + )) ?? ""), // Decrypted like other secrets. `auth.email.smtp.pass` is in `ENV_OVERRIDABLE_KEYS` so a // malformed ambient override can't abort decryption when a remote block already set it. pass: remoteWins("auth.email.smtp.pass") - ? (decryptAuthSecret( + ? ((yield* decryptAuthSecret( typeof smtpDoc["pass"] === "string" ? smtpDoc["pass"] : "", projectEnvValues, - ) ?? "") - : (decryptAuthSecret( - envOverride( + )) ?? "") + : ((yield* decryptAuthSecret( + (yield* envOverride( "SUPABASE_AUTH_EMAIL_SMTP_PASS", typeof smtpDoc["pass"] === "string" ? smtpDoc["pass"] : "", projectEnvValues, - ) ?? "", + )) ?? "", projectEnvValues, - ) ?? ""), + )) ?? ""), adminEmail: remoteWins("auth.email.smtp.admin_email") ? typeof smtpDoc["admin_email"] === "string" ? smtpDoc["admin_email"] : "" - : (envOverride( + : ((yield* envOverride( "SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL", typeof smtpDoc["admin_email"] === "string" ? smtpDoc["admin_email"] : "", projectEnvValues, - ) ?? ""), + )) ?? ""), senderName: remoteWins("auth.email.smtp.sender_name") ? typeof smtpDoc["sender_name"] === "string" ? smtpDoc["sender_name"] : undefined - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME", typeof smtpDoc["sender_name"] === "string" ? smtpDoc["sender_name"] : undefined, projectEnvValues, ), - }; -} + } satisfies SmtpInput & { readonly senderName: string | undefined }; +}); /** * `auth.captcha` validation requires both `provider` and `secret` when the section is @@ -554,26 +580,26 @@ export function resolveAuthEmailSmtp( * `authDocument` instead. Hoisted so `start.handler.ts`'s GoTrue env resolution shares this * same resolved value. */ -export function resolveAuthCaptcha( +export const resolveAuthCaptcha = Effect.fnUntraced(function* ( authDocument: Readonly> | undefined, captcha: CliConfig["auth"]["captcha"], projectEnvValues: Readonly> | undefined, /** - * Fields whose `envOverride`/`envOverrideBool` calls below can throw on a malformed value + * Fields whose `envOverride`/`envOverrideBool` calls below can fail on a malformed value * (directly, or via {@link decryptAuthSecret}) belong in this set so a matched remote * block's value wins instead of aborting on an unrelated bad env var. Defaults to empty for * callers with no `[remotes.]` block to match against. */ remoteOverrideKeys: ReadonlySet = new Set(), -): CaptchaInput | undefined { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); const captchaDoc = asRecord(authDocument?.["captcha"]); return captcha - ? { + ? ({ enabled: remoteWins("auth.captcha.enabled") ? (captcha.enabled ?? false) : captchaDoc !== undefined - ? envOverrideBool( + ? yield* envOverrideBool( "SUPABASE_AUTH_CAPTCHA_ENABLED", captcha.enabled ?? false, "auth.captcha.enabled", @@ -583,51 +609,59 @@ export function resolveAuthCaptcha( provider: remoteWins("auth.captcha.provider") ? captcha.provider : captchaDoc !== undefined - ? envOverride("SUPABASE_AUTH_CAPTCHA_PROVIDER", captcha.provider, projectEnvValues) + ? yield* envOverride( + "SUPABASE_AUTH_CAPTCHA_PROVIDER", + captcha.provider, + projectEnvValues, + ) : captcha.provider, // Decrypted like `auth.email.smtp.pass`. `auth.captcha.secret` is in // `ENV_OVERRIDABLE_KEYS` so a malformed ambient override can't abort decryption when a // remote block already set it. - secret: decryptAuthSecret( + secret: yield* decryptAuthSecret( remoteWins("auth.captcha.secret") ? captcha.secret : captchaDoc !== undefined - ? envOverride("SUPABASE_AUTH_CAPTCHA_SECRET", captcha.secret, projectEnvValues) + ? yield* envOverride("SUPABASE_AUTH_CAPTCHA_SECRET", captcha.secret, projectEnvValues) : captcha.secret, projectEnvValues, ), - } + } satisfies CaptchaInput) : undefined; -} +}); /** * Resolves the signing secret from the already-resolved `auth.jwt_secret`: empty falls back - * to {@link defaultJwtSecret}, shorter than {@link MIN_JWT_SECRET_LENGTH} throws + * to {@link defaultJwtSecret}, shorter than {@link MIN_JWT_SECRET_LENGTH} fails with * {@link InvalidJwtSecretError}. */ -export function resolveJwtSecret(configured: string | undefined): string { +export const resolveJwtSecret = Effect.fnUntraced(function* (configured: string | undefined) { if (configured === undefined || configured.length === 0) return defaultJwtSecret; if (configured.length < MIN_JWT_SECRET_LENGTH) { - throw new InvalidJwtSecretError(); + return yield* new InvalidJwtSecretError(); } return configured; -} +}); function resolveOpaqueKey(configured: string | undefined, fallback: string): string { return configured !== undefined && configured.length > 0 ? configured : fallback; } -function resolveSignedKey( +const resolveSignedKey = Effect.fnUntraced(function* ( configured: string | undefined, jwtSecret: string, signingKey: Jwk | undefined, role: "anon" | "service_role", -): string { +) { if (configured !== undefined && configured.length > 0) return configured; - return signingKey !== undefined - ? generateAsymmetricGoJwt(signingKey, role) - : generateGoJwt(jwtSecret, role); -} + return yield* Effect.try({ + try: () => + signingKey !== undefined + ? generateAsymmetricGoJwt(signingKey, role) + : generateGoJwt(jwtSecret, role), + catch: thrownError, + }); +}); /** JWK fields, matching {@link Jwk}. */ const JwkSchema = Schema.Struct({ @@ -649,41 +683,44 @@ const JwkSchema = Schema.Struct({ x: Schema.optionalKey(Schema.String), y: Schema.optionalKey(Schema.String), }); -const decodeJwks = Schema.decodeUnknownSync(Schema.Array(JwkSchema)); +const decodeJwks = Schema.decodeUnknownEffect(Schema.Array(JwkSchema)); /** - * Reads and JSON-decodes `signingKeysPath` into an array of {@link Jwk}, using `node:fs` - * directly (not the `FileSystem` Effect service) to keep this a plain synchronous resolver - * for a rarely-configured field. Callers must only invoke this when auth is enabled. + * Reads and JSON-decodes `signingKeysPath` into an array of {@link Jwk}, keeping a leading BOM + * (no BOM stripping). Callers must only invoke this when auth is enabled. */ -function readSigningKeysFile(workdir: string, signingKeysPath: string): ReadonlyArray { - const absolutePath = resolveSigningKeysPath(workdir, signingKeysPath); - - let contents: string; - try { - contents = readFileSync(absolutePath, "utf8"); - } catch (cause) { - throw new ConfigValidateError(signingKeysReadErrorMessage(cause)); - } +const readSigningKeysFile = Effect.fnUntraced(function* (workdir: string, signingKeysPath: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const absolutePath = resolveSigningKeysPath(path, workdir, signingKeysPath); + + const contents = yield* fs.readFile(absolutePath).pipe( + Effect.map((bytes) => new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes)), + Effect.mapError( + (cause) => new ConfigValidateError(signingKeysReadErrorMessage(fsErrorCause(cause))), + ), + ); - try { + const parsed = yield* Effect.try({ // oxlint-disable-next-line effecttsgo/prefer-schema-over-json -- Native parser errors are CLI output; schema decoding discards their messages. - const parsed: unknown = JSON.parse(contents); - // `Jwk.key_ops` is mutable (required for Node's `createPrivateKey`/`JsonWebKey` input), so - // it's copied into a fresh array rather than widening the schema's readonly output type. - return decodeJwks(parsed).map((jwk) => ({ - ...jwk, - key_ops: jwk.key_ops === undefined ? undefined : [...jwk.key_ops], - })); - } catch (cause) { - throw new ConfigValidateError(signingKeysDecodeErrorMessage(cause)); - } -} + try: (): unknown => JSON.parse(contents), + catch: (cause) => new ConfigValidateError(signingKeysDecodeErrorMessage(cause)), + }); + const jwks = yield* decodeJwks(parsed).pipe( + Effect.mapError((cause) => new ConfigValidateError(signingKeysDecodeErrorMessage(cause))), + ); + // `Jwk.key_ops` is mutable (required for Node's `createPrivateKey`/`JsonWebKey` input), so + // it's copied into a fresh array rather than widening the schema's readonly output type. + return jwks.map((jwk) => ({ + ...jwk, + key_ops: jwk.key_ops === undefined ? undefined : [...jwk.key_ops], + })) satisfies ReadonlyArray; +}); /** See {@link readSigningKeysFile}. */ -function loadSigningKeys(workdir: string, signingKeysPath: string): ReadonlyArray { - return readSigningKeysFile(workdir, signingKeysPath); -} +const loadSigningKeys = Effect.fnUntraced(function* (workdir: string, signingKeysPath: string) { + return yield* readSigningKeysFile(workdir, signingKeysPath); +}); /** * Returns the parsed signing keys only when auth is enabled and a path is configured; @@ -692,16 +729,16 @@ function loadSigningKeys(workdir: string, signingKeysPath: string): ReadonlyArra * on which key(s) apply. `remoteOverrideKeys` lets a matched remote block win over a * conflicting env override on `auth.enabled`/`auth.signing_keys_path`. */ -export function resolveConfiguredSigningKeys( +export const resolveConfiguredSigningKeys = Effect.fnUntraced(function* ( config: CliConfig, workdir: string, projectEnvValues: Readonly> | undefined, remoteOverrideKeys: ReadonlySet = new Set(), -): ReadonlyArray | undefined { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); const authEnabled = remoteWins("auth.enabled") ? config.auth.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_ENABLED", config.auth.enabled, "auth.enabled", @@ -709,40 +746,46 @@ export function resolveConfiguredSigningKeys( ); const signingKeysPath = remoteWins("auth.signing_keys_path") ? config.auth.signing_keys_path - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_SIGNING_KEYS_PATH", config.auth.signing_keys_path, projectEnvValues, ); return authEnabled && signingKeysPath !== undefined && signingKeysPath.length > 0 - ? loadSigningKeys(workdir, signingKeysPath) + ? yield* loadSigningKeys(workdir, signingKeysPath) : undefined; -} +}); /** * Confirms `api.tls.cert`/`.key` are readable when both are configured; the "exactly one set" * check lives in `validateResolvedConfig`. Unlike {@link readSigningKeysFile}'s * `signing_keys_path`, both paths join with the `supabase/` dir without an absolute-path guard. */ -function readApiTlsFiles( +const readApiTlsFiles = Effect.fnUntraced(function* ( workdir: string, certPath: string | undefined, keyPath: string | undefined, -): void { +) { if (certPath === undefined || certPath.length === 0) return; if (keyPath === undefined || keyPath.length === 0) return; - try { - readFileSync(resolveApiTlsPath(workdir, certPath), "utf8"); - } catch (cause) { - throw new ConfigValidateError(apiTlsCertReadErrorMessage(cause)); - } - try { - readFileSync(resolveApiTlsPath(workdir, keyPath), "utf8"); - } catch (cause) { - throw new ConfigValidateError(apiTlsKeyReadErrorMessage(cause)); - } -} + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs + .readFile(resolveApiTlsPath(path, workdir, certPath)) + .pipe( + Effect.mapError( + (cause) => new ConfigValidateError(apiTlsCertReadErrorMessage(fsErrorCause(cause))), + ), + ); + yield* fs + .readFile(resolveApiTlsPath(path, workdir, keyPath)) + .pipe( + Effect.mapError( + (cause) => new ConfigValidateError(apiTlsKeyReadErrorMessage(fsErrorCause(cause))), + ), + ); +}); /** * One `[auth.email.template.]` entry, already env-override-resolved. `subject` is @@ -780,18 +823,18 @@ export type ResolvedAuthEmail = Omit | undefined, projectEnvValues: Readonly> | undefined, /** * Fields whose `envOverride`/`envOverrideBool` calls (here or in the caller-side file read - * for `content_path`) can throw on a malformed value belong in this set so a matched remote + * for `content_path`) can fail on a malformed value belong in this set so a matched remote * block's value wins instead of aborting on an unrelated bad env var. Covers top-level * `auth.email.*` leaves as well as each `template..*`/`notification..*` leaf. */ remoteOverrideKeys: ReadonlySet = new Set(), -): ResolvedAuthEmail { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); const emailDoc = asRecord(authDocument?.["email"]); const templateDoc = asRecord(emailDoc?.["template"]); @@ -803,12 +846,12 @@ export function resolveAuthEmail( const rawSubjectPresent = asRecord(templateDoc?.[name])?.["subject"] !== undefined; const envSubject = remoteWins(`auth.email.template.${name}.subject`) ? undefined - : envOverride(`${envPrefix}_SUBJECT`, undefined, projectEnvValues); + : yield* envOverride(`${envPrefix}_SUBJECT`, undefined, projectEnvValues); template[name] = { subject: envSubject ?? (rawSubjectPresent ? tmpl.subject : undefined), content_path: remoteWins(`auth.email.template.${name}.content_path`) ? tmpl.content_path - : (envOverride(`${envPrefix}_CONTENT_PATH`, tmpl.content_path, projectEnvValues) ?? + : ((yield* envOverride(`${envPrefix}_CONTENT_PATH`, tmpl.content_path, projectEnvValues)) ?? tmpl.content_path), // `content` counts as present when set via TOML or the `_CONTENT` env override; // {@link readAuthEmailTemplateContent} rejects it below unless `content_path` is also set. @@ -816,7 +859,8 @@ export function resolveAuthEmail( asRecord(templateDoc?.[name])?.["content"] !== undefined || (remoteWins(`auth.email.template.${name}.content`) ? false - : envOverride(`${envPrefix}_CONTENT`, undefined, projectEnvValues) !== undefined), + : (yield* envOverride(`${envPrefix}_CONTENT`, undefined, projectEnvValues)) !== + undefined), }; } @@ -826,11 +870,11 @@ export function resolveAuthEmail( const rawSubjectPresent = asRecord(notificationDoc?.[name])?.["subject"] !== undefined; const envSubject = remoteWins(`auth.email.notification.${name}.subject`) ? undefined - : envOverride(`${envPrefix}_SUBJECT`, undefined, projectEnvValues); + : yield* envOverride(`${envPrefix}_SUBJECT`, undefined, projectEnvValues); notification[name] = { enabled: remoteWins(`auth.email.notification.${name}.enabled`) ? tmpl.enabled - : envOverrideBool( + : yield* envOverrideBool( `${envPrefix}_ENABLED`, tmpl.enabled, `auth.email.notification.${name}.enabled`, @@ -839,14 +883,15 @@ export function resolveAuthEmail( subject: envSubject ?? (rawSubjectPresent ? tmpl.subject : undefined), content_path: remoteWins(`auth.email.notification.${name}.content_path`) ? tmpl.content_path - : (envOverride(`${envPrefix}_CONTENT_PATH`, tmpl.content_path, projectEnvValues) ?? + : ((yield* envOverride(`${envPrefix}_CONTENT_PATH`, tmpl.content_path, projectEnvValues)) ?? tmpl.content_path), // Same `_CONTENT` env-presence fold as the template loop above. content_present: asRecord(notificationDoc?.[name])?.["content"] !== undefined || (remoteWins(`auth.email.notification.${name}.content`) ? false - : envOverride(`${envPrefix}_CONTENT`, undefined, projectEnvValues) !== undefined), + : (yield* envOverride(`${envPrefix}_CONTENT`, undefined, projectEnvValues)) !== + undefined), }; } @@ -854,7 +899,7 @@ export function resolveAuthEmail( ...email, enable_signup: remoteWins("auth.email.enable_signup") ? email.enable_signup - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_EMAIL_ENABLE_SIGNUP", email.enable_signup, "auth.email.enable_signup", @@ -862,7 +907,7 @@ export function resolveAuthEmail( ), double_confirm_changes: remoteWins("auth.email.double_confirm_changes") ? email.double_confirm_changes - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_EMAIL_DOUBLE_CONFIRM_CHANGES", email.double_confirm_changes, "auth.email.double_confirm_changes", @@ -870,7 +915,7 @@ export function resolveAuthEmail( ), enable_confirmations: remoteWins("auth.email.enable_confirmations") ? email.enable_confirmations - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_EMAIL_ENABLE_CONFIRMATIONS", email.enable_confirmations, "auth.email.enable_confirmations", @@ -878,7 +923,7 @@ export function resolveAuthEmail( ), secure_password_change: remoteWins("auth.email.secure_password_change") ? email.secure_password_change - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_EMAIL_SECURE_PASSWORD_CHANGE", email.secure_password_change, "auth.email.secure_password_change", @@ -886,11 +931,14 @@ export function resolveAuthEmail( ), max_frequency: remoteWins("auth.email.max_frequency") ? email.max_frequency - : (envOverride("SUPABASE_AUTH_EMAIL_MAX_FREQUENCY", email.max_frequency, projectEnvValues) ?? - email.max_frequency), + : ((yield* envOverride( + "SUPABASE_AUTH_EMAIL_MAX_FREQUENCY", + email.max_frequency, + projectEnvValues, + )) ?? email.max_frequency), otp_length: remoteWins("auth.email.otp_length") ? email.otp_length - : envOverrideUint( + : yield* envOverrideUint( "SUPABASE_AUTH_EMAIL_OTP_LENGTH", "auth.email.otp_length", email.otp_length, @@ -898,7 +946,7 @@ export function resolveAuthEmail( ), otp_expiry: remoteWins("auth.email.otp_expiry") ? email.otp_expiry - : envOverrideUint( + : yield* envOverrideUint( "SUPABASE_AUTH_EMAIL_OTP_EXPIRY", "auth.email.otp_expiry", email.otp_expiry, @@ -906,8 +954,8 @@ export function resolveAuthEmail( ), template, notification, - }; -} + } satisfies ResolvedAuthEmail; +}); /** * Reads each template/notification's content file, run only when auth is enabled. Every @@ -915,39 +963,61 @@ export function resolveAuthEmail( * `content`-vs-`content_path` exclusivity and path resolution live in * `resolveEmailTemplateContentPath`; this only performs the read once a path comes back. */ -function readAuthEmailTemplateContent(email: ResolvedAuthEmail, workdir: string): void { +const readAuthEmailTemplateContent = Effect.fnUntraced(function* ( + email: ResolvedAuthEmail, + workdir: string, +) { + const fs = yield* FileSystem.FileSystem; for (const [name, tmpl] of Object.entries(email.template)) { - const path = resolveEmailTemplateContentPath({ + const path = yield* resolveEmailTemplateContentPath({ section: "template", name, contentPath: tmpl.content_path, contentPresent: tmpl.content_present, base: workdir, - }); + }).pipe( + Effect.mapError((cause) => + cause instanceof ConfigValidateError ? cause : thrownError(fsErrorCause(cause)), + ), + ); if (path === undefined) continue; - try { - readFileSync(path, "utf8"); - } catch (cause) { - throw new ConfigValidateError(emailContentPathReadErrorMessage("template", name, cause)); - } + yield* fs + .readFile(path) + .pipe( + Effect.mapError( + (cause) => + new ConfigValidateError( + emailContentPathReadErrorMessage("template", name, fsErrorCause(cause)), + ), + ), + ); } for (const [name, tmpl] of Object.entries(email.notification)) { if (!tmpl.enabled) continue; - const path = resolveEmailTemplateContentPath({ + const path = yield* resolveEmailTemplateContentPath({ section: "notification", name, contentPath: tmpl.content_path, contentPresent: tmpl.content_present, base: workdir, - }); + }).pipe( + Effect.mapError((cause) => + cause instanceof ConfigValidateError ? cause : thrownError(fsErrorCause(cause)), + ), + ); if (path === undefined) continue; - try { - readFileSync(path, "utf8"); - } catch (cause) { - throw new ConfigValidateError(emailContentPathReadErrorMessage("notification", name, cause)); - } + yield* fs + .readFile(path) + .pipe( + Effect.mapError( + (cause) => + new ConfigValidateError( + emailContentPathReadErrorMessage("notification", name, fsErrorCause(cause)), + ), + ), + ); } -} +}); // Every field routed through {@link envOverrideUint} is an unsigned 64-bit value; comparing // as `BigInt` (not `Number`) avoids precision loss near `2^64`. @@ -992,33 +1062,35 @@ function parseGoBaseZeroUint(value: string): bigint | undefined { * folds an invalid or out-of-{@link UINT_MAX} override into the generic "Invalid " * error message. */ -export function envOverrideUint( +export const envOverrideUint = Effect.fnUntraced(function* ( name: string, dottedFieldPath: string, configured: number, projectEnvValues: Readonly> | undefined, -): number { - const value = envOverride(name, undefined, projectEnvValues); +) { + const value = yield* envOverride(name, undefined, projectEnvValues); if (value === undefined) return configured; const parsed = parseGoBaseZeroUint(value); if (parsed === undefined || parsed > UINT_MAX) { - throw new Error(`Failed reading config: Invalid ${dottedFieldPath}: ${value}.`); + return yield* new LocalConfigValuesError( + `Failed reading config: Invalid ${dottedFieldPath}: ${value}.`, + ); } return Number(parsed); -} +}); /** `SUPABASE_DB_MAJOR_VERSION` — see {@link envOverrideUint}. */ -export function envOverrideMajorVersion( +export const envOverrideMajorVersion = Effect.fnUntraced(function* ( configured: number, projectEnvValues: Readonly> | undefined, -): number { - return envOverrideUint( +) { + return yield* envOverrideUint( "SUPABASE_DB_MAJOR_VERSION", "db.major_version", configured, projectEnvValues, ); -} +}); const ORIOLEDB_VERSION_ENV = "SUPABASE_DB_ORIOLEDB_VERSION"; const DEPRECATED_ORIOLEDB_VERSION_ENV = "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION"; @@ -1027,22 +1099,25 @@ const DEPRECATED_ORIOLEDB_VERSION_ENV = "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION" * Reads the `db.orioledb_version` override through `read`, falling back to the deprecated * `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` alias while `SUPABASE_DB_ORIOLEDB_VERSION` is unset. */ -export const readOrioleDBVersionEnvOverride = ( +export const readOrioleDBVersionEnvOverride = Effect.fnUntraced(function* ( read: ( name: typeof ORIOLEDB_VERSION_ENV | typeof DEPRECATED_ORIOLEDB_VERSION_ENV, - ) => string | undefined, -): string | undefined => read(ORIOLEDB_VERSION_ENV) ?? read(DEPRECATED_ORIOLEDB_VERSION_ENV); + ) => Effect.Effect, +) { + return (yield* read(ORIOLEDB_VERSION_ENV)) ?? (yield* read(DEPRECATED_ORIOLEDB_VERSION_ENV)); +}); /** `SUPABASE_DB_ORIOLEDB_VERSION` or its deprecated alias — see {@link envOverride}. */ -export function envOverrideOrioleDBVersion( +export const envOverrideOrioleDBVersion = Effect.fnUntraced(function* ( configured: string | undefined, projectEnvValues: Readonly> | undefined, -): string | undefined { +) { return ( - readOrioleDBVersionEnvOverride((name) => envOverride(name, undefined, projectEnvValues)) ?? - configured + (yield* readOrioleDBVersionEnvOverride((name) => + envOverride(name, undefined, projectEnvValues), + )) ?? configured ); -} +}); /** * Set by {@link withSingleOrioleDBVersionEnvWarning}; while unset, every config read that finds the @@ -1063,27 +1138,27 @@ export const withSingleOrioleDBVersionEnvWarning = (effect: Effect.Effe * The deprecation warning to print when `read` finds `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` set, * unless an enclosing {@link withSingleOrioleDBVersionEnvWarning} already printed it. */ -export const deprecatedOrioleDBVersionEnvWarning = Effect.fnUntraced(function* ( - read: (name: typeof DEPRECATED_ORIOLEDB_VERSION_ENV) => string | undefined, +export const deprecatedOrioleDBVersionEnvWarning = Effect.fnUntraced(function* ( + read: (name: typeof DEPRECATED_ORIOLEDB_VERSION_ENV) => Effect.Effect, ) { - if (read(DEPRECATED_ORIOLEDB_VERSION_ENV) === undefined) return undefined; + if ((yield* read(DEPRECATED_ORIOLEDB_VERSION_ENV)) === undefined) return undefined; const printed = yield* OrioleDBVersionEnvWarningPrinted; if (Option.isSome(printed) && (yield* Ref.getAndSet(printed.value, true))) return undefined; return `WARN: ${DEPRECATED_ORIOLEDB_VERSION_ENV} is deprecated and will be removed in v3, use ${ORIOLEDB_VERSION_ENV} instead.`; }); /** `SUPABASE_EDGE_RUNTIME_DENO_VERSION` — see {@link envOverrideUint}. */ -export function envOverrideDenoVersion( +export const envOverrideDenoVersion = Effect.fnUntraced(function* ( configured: number, projectEnvValues: Readonly> | undefined, -): number { - return envOverrideUint( +) { + return yield* envOverrideUint( "SUPABASE_EDGE_RUNTIME_DENO_VERSION", "edge_runtime.deno_version", configured, projectEnvValues, ); -} +}); /** * Optional-uint sibling of {@link envOverrideUint} for `db.settings.*` fields @@ -1091,48 +1166,52 @@ export function envOverrideDenoVersion( * absent from config.toml. `configured`/the return value stay `number | undefined` to * preserve that state through an override miss. */ -function envOverrideOptionalUint( +const envOverrideOptionalUint = Effect.fnUntraced(function* ( name: string, dottedFieldPath: string, configured: number | undefined, projectEnvValues: Readonly> | undefined, -): number | undefined { - const value = envOverride(name, undefined, projectEnvValues); +) { + const value = yield* envOverride(name, undefined, projectEnvValues); if (value === undefined) return configured; const parsed = parseGoBaseZeroUint(value); if (parsed === undefined || parsed > UINT_MAX) { - throw new Error(`Failed reading config: Invalid ${dottedFieldPath}: ${value}.`); + return yield* new LocalConfigValuesError( + `Failed reading config: Invalid ${dottedFieldPath}: ${value}.`, + ); } return Number(parsed); -} +}); /** * Optional-bool sibling of {@link envOverrideBool} for `db.settings.track_commit_timestamp`, * the only bool field in this file left unset (not defaulted) when absent. */ -function envOverrideOptionalBool( +const envOverrideOptionalBool = Effect.fnUntraced(function* ( name: string, configured: boolean | undefined, dottedFieldPath: string, projectEnvValues: Readonly> | undefined, -): boolean | undefined { - const value = envOverride(name, undefined, projectEnvValues); +) { + const value = yield* envOverride(name, undefined, projectEnvValues); if (value === undefined) return configured; const parsed = parseGoBool(value); if (parsed === undefined) { - throw new InvalidBoolEnvOverrideError(dottedFieldPath, value); + return yield* new InvalidBoolEnvOverrideError(dottedFieldPath, value); } return parsed; -} +}); -/** Thrown by {@link resolveDbSettingsEnvOverrides} when `SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE` isn't `"origin"`, `"replica"`, or `"local"`. */ -export class InvalidSessionReplicationRoleEnvOverrideError extends Error { +/** Raised by {@link resolveDbSettingsEnvOverrides} when `SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE` isn't `"origin"`, `"replica"`, or `"local"`. */ +export class InvalidSessionReplicationRoleEnvOverrideError extends Data.Error<{ + readonly message: string; +}> { static readonly [ErrorActionabilityFingerprintId] = "InvalidSessionReplicationRoleEnvOverrideError"; constructor(dottedFieldPath: string, value: string) { - super( - `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "origin", "replica", "local"`, - ); + super({ + message: `Invalid config for ${dottedFieldPath}: cannot parse "${value}" as one of "origin", "replica", "local"`, + }); this.name = "InvalidSessionReplicationRoleEnvOverrideError"; } @@ -1146,24 +1225,24 @@ export class InvalidSessionReplicationRoleEnvOverrideError extends Error { * Unlike {@link envOverrideAnalyticsBackend}, `configured` (and the return value) may * be `undefined` — validation only runs once a value is actually present. */ -function envOverrideSessionReplicationRole( +const envOverrideSessionReplicationRole = Effect.fnUntraced(function* ( configured: string | undefined, projectEnvValues: Readonly> | undefined, -): "origin" | "replica" | "local" | undefined { - const value = envOverride( +) { + const value = yield* envOverride( "SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE", configured, projectEnvValues, ); if (value === undefined) return undefined; if (value !== "origin" && value !== "replica" && value !== "local") { - throw new InvalidSessionReplicationRoleEnvOverrideError( + return yield* new InvalidSessionReplicationRoleEnvOverrideError( "db.settings.session_replication_role", value, ); } return value; -} +}); /** * Resolves every `db.settings.*` sub-field to its env-overridden value before @@ -1171,37 +1250,37 @@ function envOverrideSessionReplicationRole( * lets a matched remote block's value win over a conflicting `SUPABASE_DB_SETTINGS_*` var, * same as elsewhere in this file. */ -export function resolveDbSettingsEnvOverrides( +export const resolveDbSettingsEnvOverrides = Effect.fnUntraced(function* ( settings: CliConfig["db"]["settings"], projectEnvValues: Readonly> | undefined, remoteOverrideKeys: ReadonlySet = new Set(), -): NonNullable { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); return { effective_cache_size: remoteWins("db.settings.effective_cache_size") ? settings?.effective_cache_size - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_EFFECTIVE_CACHE_SIZE", settings?.effective_cache_size, projectEnvValues, ), logical_decoding_work_mem: remoteWins("db.settings.logical_decoding_work_mem") ? settings?.logical_decoding_work_mem - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_LOGICAL_DECODING_WORK_MEM", settings?.logical_decoding_work_mem, projectEnvValues, ), maintenance_work_mem: remoteWins("db.settings.maintenance_work_mem") ? settings?.maintenance_work_mem - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_MAINTENANCE_WORK_MEM", settings?.maintenance_work_mem, projectEnvValues, ), max_connections: remoteWins("db.settings.max_connections") ? settings?.max_connections - : envOverrideOptionalUint( + : yield* envOverrideOptionalUint( "SUPABASE_DB_SETTINGS_MAX_CONNECTIONS", "db.settings.max_connections", settings?.max_connections, @@ -1209,7 +1288,7 @@ export function resolveDbSettingsEnvOverrides( ), max_locks_per_transaction: remoteWins("db.settings.max_locks_per_transaction") ? settings?.max_locks_per_transaction - : envOverrideOptionalUint( + : yield* envOverrideOptionalUint( "SUPABASE_DB_SETTINGS_MAX_LOCKS_PER_TRANSACTION", "db.settings.max_locks_per_transaction", settings?.max_locks_per_transaction, @@ -1217,7 +1296,7 @@ export function resolveDbSettingsEnvOverrides( ), max_parallel_maintenance_workers: remoteWins("db.settings.max_parallel_maintenance_workers") ? settings?.max_parallel_maintenance_workers - : envOverrideOptionalUint( + : yield* envOverrideOptionalUint( "SUPABASE_DB_SETTINGS_MAX_PARALLEL_MAINTENANCE_WORKERS", "db.settings.max_parallel_maintenance_workers", settings?.max_parallel_maintenance_workers, @@ -1225,7 +1304,7 @@ export function resolveDbSettingsEnvOverrides( ), max_parallel_workers: remoteWins("db.settings.max_parallel_workers") ? settings?.max_parallel_workers - : envOverrideOptionalUint( + : yield* envOverrideOptionalUint( "SUPABASE_DB_SETTINGS_MAX_PARALLEL_WORKERS", "db.settings.max_parallel_workers", settings?.max_parallel_workers, @@ -1233,7 +1312,7 @@ export function resolveDbSettingsEnvOverrides( ), max_parallel_workers_per_gather: remoteWins("db.settings.max_parallel_workers_per_gather") ? settings?.max_parallel_workers_per_gather - : envOverrideOptionalUint( + : yield* envOverrideOptionalUint( "SUPABASE_DB_SETTINGS_MAX_PARALLEL_WORKERS_PER_GATHER", "db.settings.max_parallel_workers_per_gather", settings?.max_parallel_workers_per_gather, @@ -1241,7 +1320,7 @@ export function resolveDbSettingsEnvOverrides( ), max_replication_slots: remoteWins("db.settings.max_replication_slots") ? settings?.max_replication_slots - : envOverrideOptionalUint( + : yield* envOverrideOptionalUint( "SUPABASE_DB_SETTINGS_MAX_REPLICATION_SLOTS", "db.settings.max_replication_slots", settings?.max_replication_slots, @@ -1249,31 +1328,35 @@ export function resolveDbSettingsEnvOverrides( ), max_slot_wal_keep_size: remoteWins("db.settings.max_slot_wal_keep_size") ? settings?.max_slot_wal_keep_size - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_MAX_SLOT_WAL_KEEP_SIZE", settings?.max_slot_wal_keep_size, projectEnvValues, ), max_standby_archive_delay: remoteWins("db.settings.max_standby_archive_delay") ? settings?.max_standby_archive_delay - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_MAX_STANDBY_ARCHIVE_DELAY", settings?.max_standby_archive_delay, projectEnvValues, ), max_standby_streaming_delay: remoteWins("db.settings.max_standby_streaming_delay") ? settings?.max_standby_streaming_delay - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_MAX_STANDBY_STREAMING_DELAY", settings?.max_standby_streaming_delay, projectEnvValues, ), max_wal_size: remoteWins("db.settings.max_wal_size") ? settings?.max_wal_size - : envOverride("SUPABASE_DB_SETTINGS_MAX_WAL_SIZE", settings?.max_wal_size, projectEnvValues), + : yield* envOverride( + "SUPABASE_DB_SETTINGS_MAX_WAL_SIZE", + settings?.max_wal_size, + projectEnvValues, + ), max_wal_senders: remoteWins("db.settings.max_wal_senders") ? settings?.max_wal_senders - : envOverrideOptionalUint( + : yield* envOverrideOptionalUint( "SUPABASE_DB_SETTINGS_MAX_WAL_SENDERS", "db.settings.max_wal_senders", settings?.max_wal_senders, @@ -1281,7 +1364,7 @@ export function resolveDbSettingsEnvOverrides( ), max_worker_processes: remoteWins("db.settings.max_worker_processes") ? settings?.max_worker_processes - : envOverrideOptionalUint( + : yield* envOverrideOptionalUint( "SUPABASE_DB_SETTINGS_MAX_WORKER_PROCESSES", "db.settings.max_worker_processes", settings?.max_worker_processes, @@ -1289,31 +1372,34 @@ export function resolveDbSettingsEnvOverrides( ), session_replication_role: remoteWins("db.settings.session_replication_role") ? settings?.session_replication_role - : envOverrideSessionReplicationRole(settings?.session_replication_role, projectEnvValues), + : yield* envOverrideSessionReplicationRole( + settings?.session_replication_role, + projectEnvValues, + ), shared_buffers: remoteWins("db.settings.shared_buffers") ? settings?.shared_buffers - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_SHARED_BUFFERS", settings?.shared_buffers, projectEnvValues, ), statement_timeout: remoteWins("db.settings.statement_timeout") ? settings?.statement_timeout - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_STATEMENT_TIMEOUT", settings?.statement_timeout, projectEnvValues, ), track_activity_query_size: remoteWins("db.settings.track_activity_query_size") ? settings?.track_activity_query_size - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_TRACK_ACTIVITY_QUERY_SIZE", settings?.track_activity_query_size, projectEnvValues, ), track_commit_timestamp: remoteWins("db.settings.track_commit_timestamp") ? settings?.track_commit_timestamp - : envOverrideOptionalBool( + : yield* envOverrideOptionalBool( "SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP", settings?.track_commit_timestamp, "db.settings.track_commit_timestamp", @@ -1321,23 +1407,23 @@ export function resolveDbSettingsEnvOverrides( ), wal_keep_size: remoteWins("db.settings.wal_keep_size") ? settings?.wal_keep_size - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_WAL_KEEP_SIZE", settings?.wal_keep_size, projectEnvValues, ), wal_sender_timeout: remoteWins("db.settings.wal_sender_timeout") ? settings?.wal_sender_timeout - : envOverride( + : yield* envOverride( "SUPABASE_DB_SETTINGS_WAL_SENDER_TIMEOUT", settings?.wal_sender_timeout, projectEnvValues, ), work_mem: remoteWins("db.settings.work_mem") ? settings?.work_mem - : envOverride("SUPABASE_DB_SETTINGS_WORK_MEM", settings?.work_mem, projectEnvValues), - }; -} + : yield* envOverride("SUPABASE_DB_SETTINGS_WORK_MEM", settings?.work_mem, projectEnvValues), + } satisfies NonNullable; +}); /** `password_requirements` fixed enum (`@supabase/config`'s `packages/config/src/auth/index.ts`). */ const PASSWORD_REQUIREMENTS_VALUES = new Set([ @@ -1352,16 +1438,22 @@ const PASSWORD_REQUIREMENTS_VALUES = new Set([ * `db start`'s own eager-validation battery can call it directly instead of duplicating the * check. */ -export function envOverrideAuthPasswordRequirements( +export const envOverrideAuthPasswordRequirements = Effect.fnUntraced(function* ( configured: string, projectEnvValues: Readonly> | undefined, -): string { - const override = envOverride("SUPABASE_AUTH_PASSWORD_REQUIREMENTS", undefined, projectEnvValues); +) { + const override = yield* envOverride( + "SUPABASE_AUTH_PASSWORD_REQUIREMENTS", + undefined, + projectEnvValues, + ); if (override !== undefined && !PASSWORD_REQUIREMENTS_VALUES.has(override)) { - throw new Error(`Failed reading config: Invalid auth.password_requirements: ${override}.`); + return yield* new LocalConfigValuesError( + `Failed reading config: Invalid auth.password_requirements: ${override}.`, + ); } return override ?? configured; -} +}); /** Narrows an unknown value to a plain object. */ function asRecord(value: unknown): Record | undefined { @@ -1376,18 +1468,22 @@ function asRecord(value: unknown): Record | undefined { * one-shot auth migration job so both resolve the same value. `remoteOverrideKeys` lets a * matched remote block win over a conflicting `SUPABASE_AUTH_EXTERNAL_URL`. */ -export function resolveAuthExternalUrl( +export const resolveAuthExternalUrl = Effect.fnUntraced(function* ( document: Readonly> | undefined, projectEnvValues: Readonly> | undefined, remoteOverrideKeys: ReadonlySet = new Set(), -): string | undefined { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); const rawAuthExternalUrl = asRecord(document?.["auth"])?.["external_url"]; const configuredAuthExternalUrl = typeof rawAuthExternalUrl === "string" ? rawAuthExternalUrl : undefined; if (remoteWins("auth.external_url")) return configuredAuthExternalUrl; - return envOverride("SUPABASE_AUTH_EXTERNAL_URL", configuredAuthExternalUrl, projectEnvValues); -} + return yield* envOverride( + "SUPABASE_AUTH_EXTERNAL_URL", + configuredAuthExternalUrl, + projectEnvValues, + ); +}); /** Hook-type iteration order for {@link resolveAuthHooks}'s output. */ const HOOK_TYPE_ORDER = [ @@ -1415,7 +1511,7 @@ interface ResolvedAuthHook { readonly secrets: string; } -export type ResolvedAuthHooks = { +type ResolvedAuthHooks = { readonly [K in (typeof HOOK_TYPE_TO_CAMEL)[keyof typeof HOOK_TYPE_TO_CAMEL]]: ResolvedAuthHook; }; @@ -1426,18 +1522,18 @@ export type ResolvedAuthHooks = { * `resolveLocalConfigValues` and `start.handler.ts`'s GoTrue env resolution share this same * result. */ -export function resolveAuthHooks( +export const resolveAuthHooks = Effect.fnUntraced(function* ( authDocument: Readonly> | undefined, hook: CliConfig["auth"]["hook"], projectEnvValues: Readonly> | undefined, /** - * Fields whose overrides below can throw or otherwise cause a downstream rejection on a + * Fields whose overrides below can fail or otherwise cause a downstream rejection on a * malformed value belong in this set so a matched remote block's value wins instead. A * matched remote flattens the whole `auth.hook.` block, so every leaf * (`enabled`/`uri`/`secrets`) is included, not just `enabled`. */ remoteOverrideKeys: ReadonlySet = new Set(), -): ResolvedAuthHooks { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); const hookDocument = asRecord(authDocument?.["hook"]); const result = {} as Record; @@ -1448,7 +1544,7 @@ export function resolveAuthHooks( const enabled = remoteWins(`auth.hook.${hookType}.enabled`) ? h.enabled : hookSectionPresent - ? envOverrideBool( + ? yield* envOverrideBool( `${envPrefix}_ENABLED`, h.enabled, `auth.hook.${hookType}.enabled`, @@ -1459,18 +1555,18 @@ export function resolveAuthHooks( (remoteWins(`auth.hook.${hookType}.uri`) ? h.uri : hookSectionPresent - ? envOverride(`${envPrefix}_URI`, h.uri, projectEnvValues) + ? yield* envOverride(`${envPrefix}_URI`, h.uri, projectEnvValues) : h.uri) ?? ""; const secrets = (remoteWins(`auth.hook.${hookType}.secrets`) ? h.secrets : hookSectionPresent - ? envOverride(`${envPrefix}_SECRETS`, h.secrets, projectEnvValues) + ? yield* envOverride(`${envPrefix}_SECRETS`, h.secrets, projectEnvValues) : h.secrets) ?? ""; result[HOOK_TYPE_TO_CAMEL[hookType]] = { enabled, uri, secrets }; } return result as ResolvedAuthHooks; -} +}); /** * Resolves `auth.mfa`'s per-factor fields. Unlike hooks/smtp, `auth.mfa.` is always @@ -1478,22 +1574,22 @@ export function resolveAuthHooks( * `resolveLocalConfigValues` and `start.handler.ts`'s GoTrue env resolution share this same * result. */ -export function resolveAuthMfa( +export const resolveAuthMfa = Effect.fnUntraced(function* ( mfa: CliConfig["auth"]["mfa"], projectEnvValues: Readonly> | undefined, /** - * Fields whose `envOverrideBool`/`envOverrideUint`/`envOverride` calls below can throw or + * Fields whose `envOverrideBool`/`envOverrideUint`/`envOverride` calls below can fail or * otherwise cause a downstream rejection on a malformed value belong in this set so a * matched remote block's value wins instead of aborting on an unrelated bad env var. */ remoteOverrideKeys: ReadonlySet = new Set(), -): CliConfig["auth"]["mfa"] { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); return { totp: { enroll_enabled: remoteWins("auth.mfa.totp.enroll_enabled") ? mfa.totp.enroll_enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED", mfa.totp.enroll_enabled, "auth.mfa.totp.enroll_enabled", @@ -1501,7 +1597,7 @@ export function resolveAuthMfa( ), verify_enabled: remoteWins("auth.mfa.totp.verify_enabled") ? mfa.totp.verify_enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED", mfa.totp.verify_enabled, "auth.mfa.totp.verify_enabled", @@ -1511,7 +1607,7 @@ export function resolveAuthMfa( phone: { enroll_enabled: remoteWins("auth.mfa.phone.enroll_enabled") ? mfa.phone.enroll_enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_MFA_PHONE_ENROLL_ENABLED", mfa.phone.enroll_enabled, "auth.mfa.phone.enroll_enabled", @@ -1519,7 +1615,7 @@ export function resolveAuthMfa( ), verify_enabled: remoteWins("auth.mfa.phone.verify_enabled") ? mfa.phone.verify_enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_MFA_PHONE_VERIFY_ENABLED", mfa.phone.verify_enabled, "auth.mfa.phone.verify_enabled", @@ -1527,7 +1623,7 @@ export function resolveAuthMfa( ), otp_length: remoteWins("auth.mfa.phone.otp_length") ? mfa.phone.otp_length - : envOverrideUint( + : yield* envOverrideUint( "SUPABASE_AUTH_MFA_PHONE_OTP_LENGTH", "auth.mfa.phone.otp_length", mfa.phone.otp_length, @@ -1535,20 +1631,23 @@ export function resolveAuthMfa( ), template: remoteWins("auth.mfa.phone.template") ? mfa.phone.template - : (envOverride("SUPABASE_AUTH_MFA_PHONE_TEMPLATE", mfa.phone.template, projectEnvValues) ?? - mfa.phone.template), + : ((yield* envOverride( + "SUPABASE_AUTH_MFA_PHONE_TEMPLATE", + mfa.phone.template, + projectEnvValues, + )) ?? mfa.phone.template), max_frequency: remoteWins("auth.mfa.phone.max_frequency") ? mfa.phone.max_frequency - : (envOverride( + : ((yield* envOverride( "SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY", mfa.phone.max_frequency, projectEnvValues, - ) ?? mfa.phone.max_frequency), + )) ?? mfa.phone.max_frequency), }, web_authn: { enroll_enabled: remoteWins("auth.mfa.web_authn.enroll_enabled") ? mfa.web_authn.enroll_enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_MFA_WEB_AUTHN_ENROLL_ENABLED", mfa.web_authn.enroll_enabled, "auth.mfa.web_authn.enroll_enabled", @@ -1556,7 +1655,7 @@ export function resolveAuthMfa( ), verify_enabled: remoteWins("auth.mfa.web_authn.verify_enabled") ? mfa.web_authn.verify_enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_MFA_WEB_AUTHN_VERIFY_ENABLED", mfa.web_authn.verify_enabled, "auth.mfa.web_authn.verify_enabled", @@ -1565,143 +1664,134 @@ export function resolveAuthMfa( }, max_enrolled_factors: remoteWins("auth.mfa.max_enrolled_factors") ? mfa.max_enrolled_factors - : envOverrideUint( + : yield* envOverrideUint( "SUPABASE_AUTH_MFA_MAX_ENROLLED_FACTORS", "auth.mfa.max_enrolled_factors", mfa.max_enrolled_factors, projectEnvValues, ), - }; -} + } satisfies CliConfig["auth"]["mfa"]; +}); /** * Resolves `auth.rate_limit.*`, all plain `uint`s with no presence gate: every * `SUPABASE_AUTH_RATE_LIMIT_*` override applies unconditionally. */ -export function resolveGotrueRateLimit( +export const resolveGotrueRateLimit = Effect.fnUntraced(function* ( rateLimit: CliConfig["auth"]["rate_limit"], projectEnvValues: Readonly> | undefined, -): CliConfig["auth"]["rate_limit"] { +) { return { - anonymous_users: envOverrideUint( + anonymous_users: yield* envOverrideUint( "SUPABASE_AUTH_RATE_LIMIT_ANONYMOUS_USERS", "auth.rate_limit.anonymous_users", rateLimit.anonymous_users, projectEnvValues, ), - token_refresh: envOverrideUint( + token_refresh: yield* envOverrideUint( "SUPABASE_AUTH_RATE_LIMIT_TOKEN_REFRESH", "auth.rate_limit.token_refresh", rateLimit.token_refresh, projectEnvValues, ), - sign_in_sign_ups: envOverrideUint( + sign_in_sign_ups: yield* envOverrideUint( "SUPABASE_AUTH_RATE_LIMIT_SIGN_IN_SIGN_UPS", "auth.rate_limit.sign_in_sign_ups", rateLimit.sign_in_sign_ups, projectEnvValues, ), - token_verifications: envOverrideUint( + token_verifications: yield* envOverrideUint( "SUPABASE_AUTH_RATE_LIMIT_TOKEN_VERIFICATIONS", "auth.rate_limit.token_verifications", rateLimit.token_verifications, projectEnvValues, ), - email_sent: envOverrideUint( + email_sent: yield* envOverrideUint( "SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT", "auth.rate_limit.email_sent", rateLimit.email_sent, projectEnvValues, ), - sms_sent: envOverrideUint( + sms_sent: yield* envOverrideUint( "SUPABASE_AUTH_RATE_LIMIT_SMS_SENT", "auth.rate_limit.sms_sent", rateLimit.sms_sent, projectEnvValues, ), - web3: envOverrideUint( + web3: yield* envOverrideUint( "SUPABASE_AUTH_RATE_LIMIT_WEB3", "auth.rate_limit.web3", rateLimit.web3, projectEnvValues, ), - }; -} + } satisfies CliConfig["auth"]["rate_limit"]; +}); /** * Resolves `auth.sessions.{timebox,inactivity_timeout}`. An env override can introduce a * value even when `[auth.sessions]` was never in config.toml, since `config.auth.sessions` * can be `undefined` here unlike other always-bound sections. */ -export function resolveGotrueSessions( +export const resolveGotrueSessions = Effect.fnUntraced(function* ( sessions: CliConfig["auth"]["sessions"], projectEnvValues: Readonly> | undefined, -): CliConfig["auth"]["sessions"] { - const timebox = envOverride( +) { + const timebox = yield* envOverride( "SUPABASE_AUTH_SESSIONS_TIMEBOX", sessions?.timebox, projectEnvValues, ); - const inactivityTimeout = envOverride( + const inactivityTimeout = yield* envOverride( "SUPABASE_AUTH_SESSIONS_INACTIVITY_TIMEOUT", sessions?.inactivity_timeout, projectEnvValues, ); if (timebox === undefined && inactivityTimeout === undefined) return sessions; - return { timebox, inactivity_timeout: inactivityTimeout }; -} + return { timebox, inactivity_timeout: inactivityTimeout } satisfies CliConfig["auth"]["sessions"]; +}); /** * Resolves `auth.passkey`/`auth.webauthn`, which have no `@supabase/config` schema fields at * all: presence and every field come from the raw TOML document. An absent section is never * synthesized from an env override alone. */ -export function resolveGotruePasskeyWebauthn( +export const resolveGotruePasskeyWebauthn = Effect.fnUntraced(function* ( document: Readonly> | undefined, projectEnvValues: Readonly> | undefined, -): { - readonly passkeyEnabled: boolean | undefined; - readonly webauthn: - | { - readonly rpId: string; - readonly rpDisplayName: string; - readonly rpOrigins: ReadonlyArray; - } - | undefined; -} { +) { const authDoc = asRecord(document?.["auth"]); const passkeyDoc = asRecord(authDoc?.["passkey"]); const webauthnDoc = asRecord(authDoc?.["webauthn"]); const passkeyEnabled = passkeyDoc !== undefined - ? envOverrideBool( + ? yield* envOverrideBool( "SUPABASE_AUTH_PASSKEY_ENABLED", - rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"), + yield* rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"), "auth.passkey.enabled", projectEnvValues, ) : undefined; const rpOriginsOverride = webauthnDoc !== undefined - ? envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS", undefined, projectEnvValues) + ? yield* envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS", undefined, projectEnvValues) : undefined; const webauthn = webauthnDoc !== undefined ? { rpId: - envOverride( + (yield* envOverride( "SUPABASE_AUTH_WEBAUTHN_RP_ID", typeof webauthnDoc["rp_id"] === "string" ? webauthnDoc["rp_id"] : "", projectEnvValues, - ) ?? "", + )) ?? "", rpDisplayName: - envOverride( + (yield* envOverride( "SUPABASE_AUTH_WEBAUTHN_RP_DISPLAY_NAME", typeof webauthnDoc["rp_display_name"] === "string" ? webauthnDoc["rp_display_name"] : "", projectEnvValues, - ) ?? "", + )) ?? "", // A raw or `env(...)`-resolved `rp_origins` string is comma-split, not dropped. rpOrigins: (() => { if (rpOriginsOverride !== undefined) return strToArr(rpOriginsOverride); @@ -1714,16 +1804,16 @@ export function resolveGotruePasskeyWebauthn( } : undefined; return { passkeyEnabled, webauthn }; -} +}); /** Resolves `auth.web3.*.enabled`, both value-typed with no presence gate — overrides always apply. */ -export function resolveGotrueWeb3( +export const resolveGotrueWeb3 = Effect.fnUntraced(function* ( web3: CliConfig["auth"]["web3"], projectEnvValues: Readonly> | undefined, -): CliConfig["auth"]["web3"] { +) { return { solana: { - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_AUTH_WEB3_SOLANA_ENABLED", web3.solana.enabled, "auth.web3.solana.enabled", @@ -1731,60 +1821,60 @@ export function resolveGotrueWeb3( ), }, ethereum: { - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_AUTH_WEB3_ETHEREUM_ENABLED", web3.ethereum.enabled, "auth.web3.ethereum.enabled", projectEnvValues, ), }, - }; -} + } satisfies CliConfig["auth"]["web3"]; +}); /** Resolves `auth.oauth_server.*`, value-typed with no presence gate — overrides always apply. */ -export function resolveGotrueOAuthServer( +export const resolveGotrueOAuthServer = Effect.fnUntraced(function* ( oauthServer: CliConfig["auth"]["oauth_server"], projectEnvValues: Readonly> | undefined, -): CliConfig["auth"]["oauth_server"] { +) { return { - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_AUTH_OAUTH_SERVER_ENABLED", oauthServer.enabled, "auth.oauth_server.enabled", projectEnvValues, ), authorization_url_path: - envOverride( + (yield* envOverride( "SUPABASE_AUTH_OAUTH_SERVER_AUTHORIZATION_URL_PATH", oauthServer.authorization_url_path, projectEnvValues, - ) ?? oauthServer.authorization_url_path, - allow_dynamic_registration: envOverrideBool( + )) ?? oauthServer.authorization_url_path, + allow_dynamic_registration: yield* envOverrideBool( "SUPABASE_AUTH_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION", oauthServer.allow_dynamic_registration, "auth.oauth_server.allow_dynamic_registration", projectEnvValues, ), - }; -} + } satisfies CliConfig["auth"]["oauth_server"]; +}); /** * Resolves enabled `auth.third_party.` entries in a fixed order, forwarding only * the enabled ones. `remoteOverrideKeys` lets a matched remote block's value win over a * conflicting `SUPABASE_AUTH_THIRD_PARTY_*_ENABLED`, since `envOverrideBool` would otherwise - * throw and abort the whole call. + * fail and abort the whole call. */ -export function resolveThirdPartyProviders( +export const resolveThirdPartyProviders = Effect.fnUntraced(function* ( thirdParty: CliConfig["auth"]["third_party"], projectEnvValues: Readonly> | undefined, remoteOverrideKeys: ReadonlySet = new Set(), -): ReadonlyArray { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); const resolved: Array = []; if ( remoteWins("auth.third_party.firebase.enabled") ? thirdParty.firebase.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", thirdParty.firebase.enabled, "auth.third_party.firebase.enabled", @@ -1796,7 +1886,7 @@ export function resolveThirdPartyProviders( requiredField: (remoteWins("auth.third_party.firebase.project_id") ? thirdParty.firebase.project_id - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID", thirdParty.firebase.project_id, projectEnvValues, @@ -1806,7 +1896,7 @@ export function resolveThirdPartyProviders( if ( remoteWins("auth.third_party.auth0.enabled") ? thirdParty.auth0.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_AUTH0_ENABLED", thirdParty.auth0.enabled, "auth.third_party.auth0.enabled", @@ -1818,7 +1908,7 @@ export function resolveThirdPartyProviders( requiredField: (remoteWins("auth.third_party.auth0.tenant") ? thirdParty.auth0.tenant - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT", thirdParty.auth0.tenant, projectEnvValues, @@ -1828,7 +1918,7 @@ export function resolveThirdPartyProviders( if ( remoteWins("auth.third_party.aws_cognito.enabled") ? thirdParty.aws_cognito.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_ENABLED", thirdParty.aws_cognito.enabled, "auth.third_party.aws_cognito.enabled", @@ -1840,14 +1930,14 @@ export function resolveThirdPartyProviders( requiredField: (remoteWins("auth.third_party.aws_cognito.user_pool_id") ? thirdParty.aws_cognito.user_pool_id - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_ID", thirdParty.aws_cognito.user_pool_id, projectEnvValues, )) ?? "", cognitoUserPoolRegion: remoteWins("auth.third_party.aws_cognito.user_pool_region") ? thirdParty.aws_cognito.user_pool_region - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_REGION", thirdParty.aws_cognito.user_pool_region, projectEnvValues, @@ -1857,7 +1947,7 @@ export function resolveThirdPartyProviders( if ( remoteWins("auth.third_party.clerk.enabled") ? thirdParty.clerk.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", thirdParty.clerk.enabled, "auth.third_party.clerk.enabled", @@ -1869,7 +1959,7 @@ export function resolveThirdPartyProviders( requiredField: (remoteWins("auth.third_party.clerk.domain") ? thirdParty.clerk.domain - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", thirdParty.clerk.domain, projectEnvValues, @@ -1879,7 +1969,7 @@ export function resolveThirdPartyProviders( if ( remoteWins("auth.third_party.workos.enabled") ? thirdParty.workos.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", thirdParty.workos.enabled, "auth.third_party.workos.enabled", @@ -1891,7 +1981,7 @@ export function resolveThirdPartyProviders( requiredField: (remoteWins("auth.third_party.workos.issuer_url") ? thirdParty.workos.issuer_url - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", thirdParty.workos.issuer_url, projectEnvValues, @@ -1899,7 +1989,7 @@ export function resolveThirdPartyProviders( }); } return resolved; -} +}); /** Fixed SMS provider priority — validation stops at the first enabled provider. */ const SMS_PROVIDER_ORDER = [ @@ -1916,18 +2006,18 @@ const SMS_PROVIDER_ORDER = [ * always includes an uncommented `[auth.sms.twilio]` table; the 4 top-level scalars have no * gate for the same reason. `test_otp` (a map) is left unresolved: it has no env var. */ -export function resolveAuthSms( +export const resolveAuthSms = Effect.fnUntraced(function* ( authDocument: Readonly> | undefined, sms: CliConfig["auth"]["sms"], projectEnvValues: Readonly> | undefined, /** - * Fields whose `envOverrideBool`/`decryptAuthSecret` calls below can throw on a malformed + * Fields whose `envOverrideBool`/`decryptAuthSecret` calls below can fail on a malformed * value belong in this set so a matched remote block's value wins instead of aborting on an * unrelated bad env var. Reachable from the `db diff --linked`/`db pull` shadow path via * `validateAuthSmsProviders`, not just this function's direct callers. */ remoteOverrideKeys: ReadonlySet = new Set(), -): CliConfig["auth"]["sms"] { +) { const smsDoc = asRecord(authDocument?.["sms"]); const remoteWins = makeRemoteWins(remoteOverrideKeys); @@ -1937,62 +2027,62 @@ export function resolveAuthSms( return smsDoc !== undefined && asRecord(smsDoc[providerName]) !== undefined; } - function resolveEnabled( + const resolveEnabled = Effect.fnUntraced(function* ( providerName: (typeof SMS_PROVIDER_ORDER)[number], configured: boolean, - ): boolean { + ) { if (remoteWins(`auth.sms.${providerName}.enabled`)) return configured; if (!providerPresent(providerName)) return configured; - return envOverrideBool( + return yield* envOverrideBool( `SUPABASE_AUTH_SMS_${providerName.toUpperCase()}_ENABLED`, configured, `auth.sms.${providerName}.enabled`, projectEnvValues, ); - } + }); // `remoteOverrideKey` is explicit (not reconstructed from `providerName`/`field`) since // `field`'s shape differs per provider, and not every combination is a real config key. - function resolveField( + const resolveField = Effect.fnUntraced(function* ( providerName: (typeof SMS_PROVIDER_ORDER)[number], field: string, remoteOverrideKey: RemoteOverridableKey, configured: string | undefined, - ): string | undefined { + ) { if (remoteWins(remoteOverrideKey)) return configured; if (!providerPresent(providerName)) return configured; - return envOverride( + return yield* envOverride( `SUPABASE_AUTH_SMS_${providerName.toUpperCase()}_${field.toUpperCase()}`, configured, projectEnvValues, ); - } + }); /** Resolves a provider's Secret-typed field, gated the same way `auth.email.smtp.pass` is. */ - function resolveSecretField( + const resolveSecretField = Effect.fnUntraced(function* ( providerName: (typeof SMS_PROVIDER_ORDER)[number], field: string, remoteOverrideKey: RemoteOverridableKey, configured: string | undefined, - ): string | undefined { + ) { return remoteWins(remoteOverrideKey) - ? decryptAuthSecret(configured, projectEnvValues) - : decryptAuthSecret( - resolveField(providerName, field, remoteOverrideKey, configured), + ? yield* decryptAuthSecret(configured, projectEnvValues) + : yield* decryptAuthSecret( + yield* resolveField(providerName, field, remoteOverrideKey, configured), projectEnvValues, ); - } + }); - const twilioEnabled = resolveEnabled("twilio", sms.twilio.enabled); - const twilioVerifyEnabled = resolveEnabled("twilio_verify", sms.twilio_verify.enabled); - const messagebirdEnabled = resolveEnabled("messagebird", sms.messagebird.enabled); - const textlocalEnabled = resolveEnabled("textlocal", sms.textlocal.enabled); - const vonageEnabled = resolveEnabled("vonage", sms.vonage.enabled); + const twilioEnabled = yield* resolveEnabled("twilio", sms.twilio.enabled); + const twilioVerifyEnabled = yield* resolveEnabled("twilio_verify", sms.twilio_verify.enabled); + const messagebirdEnabled = yield* resolveEnabled("messagebird", sms.messagebird.enabled); + const textlocalEnabled = yield* resolveEnabled("textlocal", sms.textlocal.enabled); + const vonageEnabled = yield* resolveEnabled("vonage", sms.vonage.enabled); const anyProviderEnabled = twilioEnabled || twilioVerifyEnabled || messagebirdEnabled || textlocalEnabled || vonageEnabled; const enableSignupConfigured = remoteWins("auth.sms.enable_signup") ? sms.enable_signup - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_SMS_ENABLE_SIGNUP", sms.enable_signup, "auth.sms.enable_signup", @@ -2005,7 +2095,7 @@ export function resolveAuthSms( enable_signup: anyProviderEnabled ? enableSignupConfigured : false, enable_confirmations: remoteWins("auth.sms.enable_confirmations") ? sms.enable_confirmations - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_SMS_ENABLE_CONFIRMATIONS", sms.enable_confirmations, "auth.sms.enable_confirmations", @@ -2013,28 +2103,32 @@ export function resolveAuthSms( ), template: remoteWins("auth.sms.template") ? sms.template - : (envOverride("SUPABASE_AUTH_SMS_TEMPLATE", sms.template, projectEnvValues) ?? sms.template), + : ((yield* envOverride("SUPABASE_AUTH_SMS_TEMPLATE", sms.template, projectEnvValues)) ?? + sms.template), max_frequency: remoteWins("auth.sms.max_frequency") ? sms.max_frequency - : (envOverride("SUPABASE_AUTH_SMS_MAX_FREQUENCY", sms.max_frequency, projectEnvValues) ?? - sms.max_frequency), + : ((yield* envOverride( + "SUPABASE_AUTH_SMS_MAX_FREQUENCY", + sms.max_frequency, + projectEnvValues, + )) ?? sms.max_frequency), twilio: { enabled: twilioEnabled, account_sid: - resolveField( + (yield* resolveField( "twilio", "account_sid", "auth.sms.twilio.account_sid", sms.twilio.account_sid, - ) ?? "", + )) ?? "", message_service_sid: - resolveField( + (yield* resolveField( "twilio", "message_service_sid", "auth.sms.twilio.message_service_sid", sms.twilio.message_service_sid, - ) ?? "", - auth_token: resolveSecretField( + )) ?? "", + auth_token: yield* resolveSecretField( "twilio", "auth_token", "auth.sms.twilio.auth_token", @@ -2043,19 +2137,19 @@ export function resolveAuthSms( }, twilio_verify: { enabled: twilioVerifyEnabled, - account_sid: resolveField( + account_sid: yield* resolveField( "twilio_verify", "account_sid", "auth.sms.twilio_verify.account_sid", sms.twilio_verify.account_sid, ), - message_service_sid: resolveField( + message_service_sid: yield* resolveField( "twilio_verify", "message_service_sid", "auth.sms.twilio_verify.message_service_sid", sms.twilio_verify.message_service_sid, ), - auth_token: resolveSecretField( + auth_token: yield* resolveSecretField( "twilio_verify", "auth_token", "auth.sms.twilio_verify.auth_token", @@ -2064,13 +2158,13 @@ export function resolveAuthSms( }, messagebird: { enabled: messagebirdEnabled, - originator: resolveField( + originator: yield* resolveField( "messagebird", "originator", "auth.sms.messagebird.originator", sms.messagebird.originator, ), - access_key: resolveSecretField( + access_key: yield* resolveSecretField( "messagebird", "access_key", "auth.sms.messagebird.access_key", @@ -2079,13 +2173,13 @@ export function resolveAuthSms( }, textlocal: { enabled: textlocalEnabled, - sender: resolveField( + sender: yield* resolveField( "textlocal", "sender", "auth.sms.textlocal.sender", sms.textlocal.sender, ), - api_key: resolveSecretField( + api_key: yield* resolveSecretField( "textlocal", "api_key", "auth.sms.textlocal.api_key", @@ -2094,72 +2188,81 @@ export function resolveAuthSms( }, vonage: { enabled: vonageEnabled, - from: resolveField("vonage", "from", "auth.sms.vonage.from", sms.vonage.from), - api_key: resolveField("vonage", "api_key", "auth.sms.vonage.api_key", sms.vonage.api_key), - api_secret: resolveSecretField( + from: yield* resolveField("vonage", "from", "auth.sms.vonage.from", sms.vonage.from), + api_key: yield* resolveField( + "vonage", + "api_key", + "auth.sms.vonage.api_key", + sms.vonage.api_key, + ), + api_secret: yield* resolveSecretField( "vonage", "api_secret", "auth.sms.vonage.api_secret", sms.vonage.api_secret, ), }, - }; -} + } satisfies CliConfig["auth"]["sms"]; +}); /** * Validates only the first enabled provider in {@link SMS_PROVIDER_ORDER}; a later * enabled-but-incomplete provider is never checked. Runs against {@link resolveAuthSms}'s * env-override-aware result. */ -function validateAuthSmsProviders( +const validateAuthSmsProviders = Effect.fnUntraced(function* ( authDocument: Record | undefined, sms: CliConfig["auth"]["sms"], projectEnvValues: Readonly> | undefined, remoteOverrideKeys: ReadonlySet = new Set(), -): void { - const resolved = resolveAuthSms(authDocument, sms, projectEnvValues, remoteOverrideKeys); +) { + const resolved = yield* resolveAuthSms(authDocument, sms, projectEnvValues, remoteOverrideKeys); - function requireField(provider: string, field: string, value: string | undefined): void { + const requireField = Effect.fnUntraced(function* ( + provider: string, + field: string, + value: string | undefined, + ) { if (value === undefined || value.length === 0) { - throw new ConfigValidateError( + return yield* new ConfigValidateError( `Missing required field in config: auth.sms.${provider}.${field}`, ); } - } + }); if (resolved.twilio.enabled) { - requireField("twilio", "account_sid", resolved.twilio.account_sid); - requireField("twilio", "message_service_sid", resolved.twilio.message_service_sid); - requireField("twilio", "auth_token", resolved.twilio.auth_token); + yield* requireField("twilio", "account_sid", resolved.twilio.account_sid); + yield* requireField("twilio", "message_service_sid", resolved.twilio.message_service_sid); + yield* requireField("twilio", "auth_token", resolved.twilio.auth_token); return; } if (resolved.twilio_verify.enabled) { - requireField("twilio_verify", "account_sid", resolved.twilio_verify.account_sid); - requireField( + yield* requireField("twilio_verify", "account_sid", resolved.twilio_verify.account_sid); + yield* requireField( "twilio_verify", "message_service_sid", resolved.twilio_verify.message_service_sid, ); - requireField("twilio_verify", "auth_token", resolved.twilio_verify.auth_token); + yield* requireField("twilio_verify", "auth_token", resolved.twilio_verify.auth_token); return; } if (resolved.messagebird.enabled) { - requireField("messagebird", "originator", resolved.messagebird.originator); - requireField("messagebird", "access_key", resolved.messagebird.access_key); + yield* requireField("messagebird", "originator", resolved.messagebird.originator); + yield* requireField("messagebird", "access_key", resolved.messagebird.access_key); return; } if (resolved.textlocal.enabled) { - requireField("textlocal", "sender", resolved.textlocal.sender); - requireField("textlocal", "api_key", resolved.textlocal.api_key); + yield* requireField("textlocal", "sender", resolved.textlocal.sender); + yield* requireField("textlocal", "api_key", resolved.textlocal.api_key); return; } if (resolved.vonage.enabled) { - requireField("vonage", "from", resolved.vonage.from); - requireField("vonage", "api_key", resolved.vonage.api_key); - requireField("vonage", "api_secret", resolved.vonage.api_secret); + yield* requireField("vonage", "from", resolved.vonage.from); + yield* requireField("vonage", "api_key", resolved.vonage.api_key); + yield* requireField("vonage", "api_secret", resolved.vonage.api_secret); return; } -} +}); /** Deleted external providers, warned on if still enabled, never validated. */ const DEPRECATED_EXTERNAL_PROVIDERS = new Set(["linkedin", "slack"]); @@ -2180,21 +2283,24 @@ export interface ResolvedAuthExternalProvider { * `auth.external` providers, `auth.passkey`/`auth.webauthn`) to a bool, since an `env(VAR)` * substitution there skips normal type coercion and leaves a literal `"true"`/`"false"` * string. A number coerces via truthiness (`!= 0`); an unparsable string or any other type - * throws rather than silently defaulting to `false`. + * fails rather than silently defaulting to `false`. */ -export function rawUnmodeledBool(value: unknown, dottedFieldPath: string): boolean { +export const rawUnmodeledBool = Effect.fnUntraced(function* ( + value: unknown, + dottedFieldPath: string, +) { if (value === undefined) return false; if (typeof value === "boolean") return value; if (typeof value === "number") return value !== 0; if (typeof value === "string") { const parsed = parseGoBool(value); if (parsed === undefined) { - throw new InvalidBoolEnvOverrideError(dottedFieldPath, value); + return yield* new InvalidBoolEnvOverrideError(dottedFieldPath, value); } return parsed; } - throw new InvalidBoolEnvOverrideError(dottedFieldPath, String(value)); -} + return yield* new InvalidBoolEnvOverrideError(dottedFieldPath, String(value)); +}); /** * Comma-splits a string into an array (empty string → `[]`), the same rule config decode @@ -2212,18 +2318,18 @@ export function strToArr(value: string): Array { * uncommented. Hoisted so both {@link validateAuthExternalProviders} and `start.handler.ts`'s * GoTrue env resolution share this same result. */ -export function resolveAuthExternalProviders( +export const resolveAuthExternalProviders = Effect.fnUntraced(function* ( authDocument: Readonly> | undefined, external: CliConfig["auth"]["external"], projectEnvValues: Readonly> | undefined, /** - * Fields whose `envOverrideBool`/`decryptAuthSecret` calls below can throw on a malformed + * Fields whose `envOverrideBool`/`decryptAuthSecret` calls below can fail on a malformed * value belong in this set so a matched remote block's value wins instead of aborting on an * unrelated bad env var. Provider names are dynamic, so these keys are tracked via * `applyRemoteOverride` rather than a fixed `ENV_OVERRIDABLE_KEYS` entry. */ remoteOverrideKeys: ReadonlySet = new Set(), -): Record { +) { const remoteWins = makeRemoteWins(remoteOverrideKeys); const externalDoc = asRecord(authDocument?.["external"]); @@ -2240,7 +2346,7 @@ export function resolveAuthExternalProviders( if (provider === undefined && rawProvider === undefined) continue; const configuredEnabled = provider?.enabled ?? - rawUnmodeledBool(rawProvider?.["enabled"], `auth.external.${name}.enabled`); + (yield* rawUnmodeledBool(rawProvider?.["enabled"], `auth.external.${name}.enabled`)); const configuredClientId = provider?.client_id ?? (typeof rawProvider?.["client_id"] === "string" ? rawProvider["client_id"] : undefined); @@ -2254,15 +2360,21 @@ export function resolveAuthExternalProviders( (typeof rawProvider?.["redirect_uri"] === "string" ? rawProvider["redirect_uri"] : undefined); const configuredSkipNonceCheck = provider?.skip_nonce_check ?? - rawUnmodeledBool(rawProvider?.["skip_nonce_check"], `auth.external.${name}.skip_nonce_check`); + (yield* rawUnmodeledBool( + rawProvider?.["skip_nonce_check"], + `auth.external.${name}.skip_nonce_check`, + )); const configuredEmailOptional = provider?.email_optional ?? - rawUnmodeledBool(rawProvider?.["email_optional"], `auth.external.${name}.email_optional`); + (yield* rawUnmodeledBool( + rawProvider?.["email_optional"], + `auth.external.${name}.email_optional`, + )); result[name] = { enabled: remoteWins(`auth.external.${name}.enabled`) ? configuredEnabled - : envOverrideBool( + : yield* envOverrideBool( `${envPrefix}_ENABLED`, configuredEnabled, `auth.external.${name}.enabled`, @@ -2271,23 +2383,24 @@ export function resolveAuthExternalProviders( clientId: (remoteWins(`auth.external.${name}.client_id`) ? configuredClientId - : envOverride(`${envPrefix}_CLIENT_ID`, configuredClientId, projectEnvValues)) ?? "", + : yield* envOverride(`${envPrefix}_CLIENT_ID`, configuredClientId, projectEnvValues)) ?? + "", secret: remoteWins(`auth.external.${name}.secret`) - ? decryptAuthSecret(configuredSecret, projectEnvValues) - : decryptAuthSecret( - envOverride(`${envPrefix}_SECRET`, configuredSecret, projectEnvValues), + ? yield* decryptAuthSecret(configuredSecret, projectEnvValues) + : yield* decryptAuthSecret( + yield* envOverride(`${envPrefix}_SECRET`, configuredSecret, projectEnvValues), projectEnvValues, ), url: (remoteWins(`auth.external.${name}.url`) ? configuredUrl - : envOverride(`${envPrefix}_URL`, configuredUrl, projectEnvValues)) ?? "", + : yield* envOverride(`${envPrefix}_URL`, configuredUrl, projectEnvValues)) ?? "", redirectUri: remoteWins(`auth.external.${name}.redirect_uri`) ? configuredRedirectUri - : envOverride(`${envPrefix}_REDIRECT_URI`, configuredRedirectUri, projectEnvValues), + : yield* envOverride(`${envPrefix}_REDIRECT_URI`, configuredRedirectUri, projectEnvValues), skipNonceCheck: remoteWins(`auth.external.${name}.skip_nonce_check`) ? configuredSkipNonceCheck - : envOverrideBool( + : yield* envOverrideBool( `${envPrefix}_SKIP_NONCE_CHECK`, configuredSkipNonceCheck, `auth.external.${name}.skip_nonce_check`, @@ -2295,7 +2408,7 @@ export function resolveAuthExternalProviders( ), emailOptional: remoteWins(`auth.external.${name}.email_optional`) ? configuredEmailOptional - : envOverrideBool( + : yield* envOverrideBool( `${envPrefix}_EMAIL_OPTIONAL`, configuredEmailOptional, `auth.external.${name}.email_optional`, @@ -2304,7 +2417,7 @@ export function resolveAuthExternalProviders( }; } return result; -} +}); /** * Validates required fields for every enabled `auth.external.` provider, including @@ -2312,13 +2425,13 @@ export function resolveAuthExternalProviders( * {@link resolveAuthExternalProviders}'s env-override-aware result, since the schema's own * `requiredWhenEnabled` check only sees the pre-override, known-providers-only value. */ -function validateAuthExternalProviders( +const validateAuthExternalProviders = Effect.fnUntraced(function* ( authDocument: Record | undefined, external: CliConfig["auth"]["external"], projectEnvValues: Readonly> | undefined, remoteOverrideKeys: ReadonlySet = new Set(), -): void { - const resolved = resolveAuthExternalProviders( +) { + const resolved = yield* resolveAuthExternalProviders( authDocument, external, projectEnvValues, @@ -2327,7 +2440,7 @@ function validateAuthExternalProviders( for (const [name, provider] of Object.entries(resolved)) { if (!provider.enabled) continue; if (provider.clientId.length === 0) { - throw new ConfigValidateError( + return yield* new ConfigValidateError( `Missing required field in config: auth.external.${name}.client_id`, ); } @@ -2336,36 +2449,36 @@ function validateAuthExternalProviders( name !== "google" && (provider.secret === undefined || provider.secret.length === 0) ) { - throw new ConfigValidateError( + return yield* new ConfigValidateError( `Missing required field in config: auth.external.${name}.secret`, ); } } -} +}); /** - * @throws when `project_id` (post-override, post-workdir-basename-fallback) is an explicit + * Fails when `project_id` (post-override, post-workdir-basename-fallback) is an explicit * empty string. Checked first: the sanitized workdir basename is merged in as a default * before `config.toml`, so `project_id` is never empty by the time validation runs — a * workdir whose basename sanitizes to `""` fails even with no `project_id` key at all. - * @throws {InvalidJwtSecretError} when `auth.jwt_secret` is set but too short. - * @throws {InvalidPortEnvOverrideError} when a `SUPABASE_*_PORT` env/dotenv override doesn't - * parse as a valid port. - * @throws {InvalidBoolEnvOverrideError} when a `SUPABASE_*_ENABLED` env/dotenv override - * doesn't parse as a valid bool. - * @throws when a configured `api.tls` cert/key file can't be read — see + * Fails with {@link InvalidJwtSecretError} when `auth.jwt_secret` is set but too short. + * Fails with {@link InvalidPortEnvOverrideError} when a `SUPABASE_*_PORT` env/dotenv + * override doesn't parse as a valid port. + * Fails with {@link InvalidBoolEnvOverrideError} when a `SUPABASE_*_ENABLED` env/dotenv + * override doesn't parse as a valid bool. + * Fails when a configured `api.tls` cert/key file can't be read — see * {@link readApiTlsFiles}. - * @throws when `auth.signing_keys_path` is set, auth is enabled, and the file is missing, + * Fails when `auth.signing_keys_path` is set, auth is enabled, and the file is missing, * malformed, or its first key uses an unsupported algorithm — see * {@link resolveConfiguredSigningKeys} and {@link generateAsymmetricGoJwt}. - * @throws when an email template's `content` is present without `content_path`, or a + * Fails when an email template's `content` is present without `content_path`, or a * configured `content_path` file can't be read — see {@link readAuthEmailTemplateContent}. - * @throws {InvalidAnalyticsBackendEnvOverrideError} when `SUPABASE_ANALYTICS_BACKEND` doesn't - * parse as one of its accepted values. - * @throws {ConfigValidateError} for every other validation branch, deferred to a single call - * to {@link validateResolvedConfig} at the end of this function. + * Fails with {@link InvalidAnalyticsBackendEnvOverrideError} when `SUPABASE_ANALYTICS_BACKEND` + * doesn't parse as one of its accepted values. + * Fails with {@link ConfigValidateError} for every other validation branch, deferred to a + * single call to {@link validateResolvedConfig} at the end of this function. */ -export function resolveLocalConfigValues( +export const resolveLocalConfigValues = Effect.fnUntraced(function* ( config: CliConfig, hostname: string, workdir: string, @@ -2378,9 +2491,9 @@ export function resolveLocalConfigValues( document?: Readonly>, /** * Config keys a matched `[remotes.]` block set at override tier. Every field whose - * `envOverride*` call (or a downstream {@link validateResolvedConfig} check) can throw on a - * malformed value belongs in this set — this function returns its whole object or throws, so - * an unconditional throw anywhere aborts the whole call, even for a field not part of the + * `envOverride*` call (or a downstream {@link validateResolvedConfig} check) can fail on a + * malformed value belongs in this set — this function returns its whole object or fails, so + * an unconditional failure anywhere aborts the whole call, even for a field not part of the * returned `LocalConfigValues`. Defaults to empty for callers with no `[remotes.]` block. */ remoteOverrideKeys: ReadonlySet = new Set(), @@ -2389,7 +2502,8 @@ export function resolveLocalConfigValues( * basename fallback applies. `undefined` for `status`/`stop`, which have no such flag. */ projectIdFallback?: string, -): LocalConfigValues { +) { + const path = yield* Path.Path; const remoteWins = makeRemoteWins(remoteOverrideKeys); // Checked first, before every other field: `config.project_id` is `undefined` only when the // key is absent, in which case the sanitized workdir basename (or @@ -2398,18 +2512,18 @@ export function resolveLocalConfigValues( // Not gated by `remoteWins("project_id")` like the fields below: `envOverride` never turns // an already non-empty remote-merged value empty or vice versa, so gating here couldn't // change the accept/reject outcome. - const resolvedProjectId = envOverride( + const resolvedProjectId = yield* envOverride( "SUPABASE_PROJECT_ID", config.project_id ?? (projectIdFallback !== undefined && projectIdFallback.length > 0 ? projectIdFallback - : sanitizeProjectId(basename(workdir))), + : sanitizeProjectId(path.basename(workdir))), projectEnvValues, ); const apiTlsEnabled = remoteWins("api.tls.enabled") ? config.api.tls.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_API_TLS_ENABLED", config.api.tls.enabled, "api.tls.enabled", @@ -2419,26 +2533,35 @@ export function resolveLocalConfigValues( // value, not raw `config.api.enabled`. const apiEnabled = remoteWins("api.enabled") ? config.api.enabled - : envOverrideBool("SUPABASE_API_ENABLED", config.api.enabled, "api.enabled", projectEnvValues); + : yield* envOverrideBool( + "SUPABASE_API_ENABLED", + config.api.enabled, + "api.enabled", + projectEnvValues, + ); const apiTlsCertPath = remoteWins("api.tls.cert_path") ? config.api.tls.cert_path - : envOverride("SUPABASE_API_TLS_CERT_PATH", config.api.tls.cert_path, projectEnvValues); + : yield* envOverride("SUPABASE_API_TLS_CERT_PATH", config.api.tls.cert_path, projectEnvValues); const apiTlsKeyPath = remoteWins("api.tls.key_path") ? config.api.tls.key_path - : envOverride("SUPABASE_API_TLS_KEY_PATH", config.api.tls.key_path, projectEnvValues); + : yield* envOverride("SUPABASE_API_TLS_KEY_PATH", config.api.tls.key_path, projectEnvValues); if (apiEnabled && apiTlsEnabled) { - readApiTlsFiles(workdir, apiTlsCertPath, apiTlsKeyPath); + yield* readApiTlsFiles(workdir, apiTlsCertPath, apiTlsKeyPath); } // `api.port === 0` is rejected only when `api.enabled`, unlike `db.port` below. Resolved once // so the check and the URL derivation below share the same overridden value. const apiPort = remoteWins("api.port") ? config.api.port - : envOverridePort("SUPABASE_API_PORT", config.api.port, "api.port", projectEnvValues); + : yield* envOverridePort("SUPABASE_API_PORT", config.api.port, "api.port", projectEnvValues); const apiExternalUrl = resolveApiExternalUrl( { external_url: remoteWins("api.external_url") ? config.api.external_url - : envOverride("SUPABASE_API_EXTERNAL_URL", config.api.external_url, projectEnvValues), + : yield* envOverride( + "SUPABASE_API_EXTERNAL_URL", + config.api.external_url, + projectEnvValues, + ), port: apiPort, tls: { enabled: apiTlsEnabled }, }, @@ -2448,19 +2571,19 @@ export function resolveLocalConfigValues( // it's unconditionally required, and a decoded `0` fails validation. const dbPort = remoteWins("db.port") ? config.db.port - : envOverridePort("SUPABASE_DB_PORT", config.db.port, "db.port", projectEnvValues); + : yield* envOverridePort("SUPABASE_DB_PORT", config.db.port, "db.port", projectEnvValues); // Validate-only: the shadow's own resolved `majorVersion` comes from // `resolveDbBootstrapConfig`, which gates it separately. const majorVersion = remoteWins("db.major_version") ? config.db.major_version - : envOverrideMajorVersion(config.db.major_version, projectEnvValues); + : yield* envOverrideMajorVersion(config.db.major_version, projectEnvValues); // Validate-only: the actual resolved settings `start` needs are recomputed at their own call // site. - resolveDbSettingsEnvOverrides(config.db.settings, projectEnvValues, remoteOverrideKeys); + yield* resolveDbSettingsEnvOverrides(config.db.settings, projectEnvValues, remoteOverrideKeys); // Validate-only: `start` doesn't otherwise consume `db.network_restrictions.enabled` (only // `config push` does). if (!remoteWins("db.network_restrictions.enabled")) { - envOverrideBool( + yield* envOverrideBool( "SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED", config.db.network_restrictions.enabled, "db.network_restrictions.enabled", @@ -2472,24 +2595,24 @@ export function resolveLocalConfigValues( // `/etc/postgresql-custom/pgsodium_root.key` on every start. const rawRootKeyValue = asRecord(document?.["db"])?.["root_key"]; if (rawRootKeyValue !== undefined && typeof rawRootKeyValue !== "string") { - throw new ConfigValidateError( + return yield* new ConfigValidateError( "failed to parse config: decoding failed due to the following error(s):\n\n'db.root_key' expected a map or struct", ); } const rawRootKey = remoteWins("db.root_key") ? rawRootKeyValue - : envOverride("SUPABASE_DB_ROOT_KEY", rawRootKeyValue, projectEnvValues); + : yield* envOverride("SUPABASE_DB_ROOT_KEY", rawRootKeyValue, projectEnvValues); const rootKey = rawRootKey === undefined || rawRootKey.length === 0 ? POSTGRES_DEFAULT_ROOT_KEY - : (decryptAuthSecret(rawRootKey, projectEnvValues) ?? POSTGRES_DEFAULT_ROOT_KEY); + : ((yield* decryptAuthSecret(rawRootKey, projectEnvValues)) ?? POSTGRES_DEFAULT_ROOT_KEY); const storageBucketNames = config.storage.buckets !== undefined ? Object.keys(config.storage.buckets) : []; // `studio.port === 0` is rejected only when `studio.enabled`, same enabled-gated pattern as // `api.port` above. const studioEnabled = remoteWins("studio.enabled") ? config.studio.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_STUDIO_ENABLED", config.studio.enabled, "studio.enabled", @@ -2497,19 +2620,24 @@ export function resolveLocalConfigValues( ); const studioPort = remoteWins("studio.port") ? config.studio.port - : envOverridePort("SUPABASE_STUDIO_PORT", config.studio.port, "studio.port", projectEnvValues); - // `envOverride` itself never throws, but `studio.api_url` feeds `validateResolvedConfig`'s + : yield* envOverridePort( + "SUPABASE_STUDIO_PORT", + config.studio.port, + "studio.port", + projectEnvValues, + ); + // `envOverride` itself never fails, but `studio.api_url` feeds `validateResolvedConfig`'s // URL-parse check below, which does throw on a malformed URL, so this is gated the same way // as `studio.enabled`/`studio.port` above. const studioApiUrl = remoteWins("studio.api_url") ? config.studio.api_url - : (envOverride("SUPABASE_STUDIO_API_URL", config.studio.api_url, projectEnvValues) ?? + : ((yield* envOverride("SUPABASE_STUDIO_API_URL", config.studio.api_url, projectEnvValues)) ?? config.studio.api_url); // `local_smtp.port === 0` is rejected only when `local_smtp.enabled`; the deprecated // `inbucket.enabled` alias is the same underlying flag, not a second one. const mailpitEnabled = remoteWins("local_smtp.enabled") ? config.local_smtp.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_LOCAL_SMTP_ENABLED", config.local_smtp.enabled, "local_smtp.enabled", @@ -2517,23 +2645,23 @@ export function resolveLocalConfigValues( ); const mailpitPort = remoteWins("local_smtp.port") ? config.local_smtp.port - : envOverridePort( + : yield* envOverridePort( "SUPABASE_LOCAL_SMTP_PORT", config.local_smtp.port, "local_smtp.port", projectEnvValues, ); - const jwtSecret = resolveJwtSecret( - decryptAuthSecret( + const jwtSecret = yield* resolveJwtSecret( + yield* decryptAuthSecret( remoteWins("auth.jwt_secret") ? config.auth.jwt_secret - : envOverride("SUPABASE_AUTH_JWT_SECRET", config.auth.jwt_secret, projectEnvValues), + : yield* envOverride("SUPABASE_AUTH_JWT_SECRET", config.auth.jwt_secret, projectEnvValues), projectEnvValues, ), ); const signingKeysPath = remoteWins("auth.signing_keys_path") ? config.auth.signing_keys_path - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_SIGNING_KEYS_PATH", config.auth.signing_keys_path, projectEnvValues, @@ -2543,7 +2671,7 @@ export function resolveLocalConfigValues( // and anon/service_role key generation run unconditionally either way. const authEnabled = remoteWins("auth.enabled") ? config.auth.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_ENABLED", config.auth.enabled, "auth.enabled", @@ -2554,14 +2682,14 @@ export function resolveLocalConfigValues( // `db.port === 0` above. const siteUrl = remoteWins("auth.site_url") ? config.auth.site_url - : (envOverride("SUPABASE_AUTH_SITE_URL", config.auth.site_url, projectEnvValues) ?? + : ((yield* envOverride("SUPABASE_AUTH_SITE_URL", config.auth.site_url, projectEnvValues)) ?? config.auth.site_url); const jwtIssuer = remoteWins("auth.jwt_issuer") ? config.auth.jwt_issuer - : envOverride("SUPABASE_AUTH_JWT_ISSUER", config.auth.jwt_issuer, projectEnvValues); + : yield* envOverride("SUPABASE_AUTH_JWT_ISSUER", config.auth.jwt_issuer, projectEnvValues); const jwtExpiry = remoteWins("auth.jwt_expiry") ? config.auth.jwt_expiry - : envOverrideUint( + : yield* envOverrideUint( "SUPABASE_AUTH_JWT_EXPIRY", "auth.jwt_expiry", config.auth.jwt_expiry, @@ -2569,14 +2697,14 @@ export function resolveLocalConfigValues( ); const additionalRedirectUrlsOverride = remoteWins("auth.additional_redirect_urls") ? undefined - : envOverride("SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS", undefined, projectEnvValues); + : yield* envOverride("SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS", undefined, projectEnvValues); const additionalRedirectUrls = additionalRedirectUrlsOverride !== undefined ? additionalRedirectUrlsOverride.split(",") : config.auth.additional_redirect_urls; const enableSignup = remoteWins("auth.enable_signup") ? config.auth.enable_signup - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_ENABLE_SIGNUP", config.auth.enable_signup, "auth.enable_signup", @@ -2584,7 +2712,7 @@ export function resolveLocalConfigValues( ); const enableAnonymousSignIns = remoteWins("auth.enable_anonymous_sign_ins") ? config.auth.enable_anonymous_sign_ins - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS", config.auth.enable_anonymous_sign_ins, "auth.enable_anonymous_sign_ins", @@ -2592,7 +2720,7 @@ export function resolveLocalConfigValues( ); const enableRefreshTokenRotation = remoteWins("auth.enable_refresh_token_rotation") ? config.auth.enable_refresh_token_rotation - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION", config.auth.enable_refresh_token_rotation, "auth.enable_refresh_token_rotation", @@ -2600,7 +2728,7 @@ export function resolveLocalConfigValues( ); const refreshTokenReuseInterval = remoteWins("auth.refresh_token_reuse_interval") ? config.auth.refresh_token_reuse_interval - : envOverrideUint( + : yield* envOverrideUint( "SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL", "auth.refresh_token_reuse_interval", config.auth.refresh_token_reuse_interval, @@ -2608,7 +2736,7 @@ export function resolveLocalConfigValues( ); const enableManualLinking = remoteWins("auth.enable_manual_linking") ? config.auth.enable_manual_linking - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_AUTH_ENABLE_MANUAL_LINKING", config.auth.enable_manual_linking, "auth.enable_manual_linking", @@ -2616,7 +2744,7 @@ export function resolveLocalConfigValues( ); const minimumPasswordLength = remoteWins("auth.minimum_password_length") ? config.auth.minimum_password_length - : envOverrideUint( + : yield* envOverrideUint( "SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH", "auth.minimum_password_length", config.auth.minimum_password_length, @@ -2624,9 +2752,12 @@ export function resolveLocalConfigValues( ); const passwordRequirements = remoteWins("auth.password_requirements") ? config.auth.password_requirements - : envOverrideAuthPasswordRequirements(config.auth.password_requirements, projectEnvValues); + : yield* envOverrideAuthPasswordRequirements( + config.auth.password_requirements, + projectEnvValues, + ); const authDocument = asRecord(document?.["auth"]); - const captchaInput = resolveAuthCaptcha( + const captchaInput = yield* resolveAuthCaptcha( authDocument, config.auth.captcha, projectEnvValues, @@ -2638,9 +2769,12 @@ export function resolveLocalConfigValues( // to symmetric HS256. const signingKey = signingKeysPath !== undefined && signingKeysPath.length > 0 - ? (resolveConfiguredSigningKeys(config, workdir, projectEnvValues, remoteOverrideKeys) ?? [ - DEFAULT_SIGNING_KEY, - ])[0] + ? ((yield* resolveConfiguredSigningKeys( + config, + workdir, + projectEnvValues, + remoteOverrideKeys, + )) ?? [DEFAULT_SIGNING_KEY])[0] : undefined; // This block only accumulates the inputs passkey/webauthn/hook/mfa/email/smtp/third_party // validation needs; the checks themselves run once, later, in the single @@ -2657,11 +2791,11 @@ export function resolveLocalConfigValues( // config are env-bindable, so an absent `[auth.passkey]`/`[auth.webauthn]` section is never // synthesized from an env override alone. const passkeyEnabled = remoteWins("auth.passkey.enabled") - ? rawUnmodeledBool(passkeyDoc?.["enabled"], "auth.passkey.enabled") + ? yield* rawUnmodeledBool(passkeyDoc?.["enabled"], "auth.passkey.enabled") : passkeyDoc !== undefined - ? envOverrideBool( + ? yield* envOverrideBool( "SUPABASE_AUTH_PASSKEY_ENABLED", - rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"), + yield* rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"), "auth.passkey.enabled", projectEnvValues, ) @@ -2671,12 +2805,12 @@ export function resolveLocalConfigValues( const rpId = remoteWins("auth.webauthn.rp_id") ? configuredRpId : webauthnDoc !== undefined - ? envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ID", configuredRpId, projectEnvValues) + ? yield* envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ID", configuredRpId, projectEnvValues) : undefined; const rpOriginsOverride = remoteWins("auth.webauthn.rp_origins") ? undefined : webauthnDoc !== undefined - ? envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS", undefined, projectEnvValues) + ? yield* envOverride("SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS", undefined, projectEnvValues) : undefined; // A raw or `env(...)`-resolved `rp_origins` string is comma-split, not dropped. const rawRpOrigins = webauthnDoc?.["rp_origins"]; @@ -2693,7 +2827,7 @@ export function resolveLocalConfigValues( : undefined; // Only enabled hooks are forwarded, in {@link HOOK_TYPE_ORDER}. - const resolvedHooks = resolveAuthHooks( + const resolvedHooks = yield* resolveAuthHooks( authDocument, config.auth.hook, projectEnvValues, @@ -2706,7 +2840,11 @@ export function resolveLocalConfigValues( return { type: hookType, uri: resolved.uri, secrets: resolved.secrets }; }); - const resolvedMfa = resolveAuthMfa(config.auth.mfa, projectEnvValues, remoteOverrideKeys); + const resolvedMfa = yield* resolveAuthMfa( + config.auth.mfa, + projectEnvValues, + remoteOverrideKeys, + ); const mfa: ReadonlyArray = [ { label: "totp", @@ -2725,12 +2863,21 @@ export function resolveLocalConfigValues( }, ]; - readAuthEmailTemplateContent( - resolveAuthEmail(config.auth.email, authDocument, projectEnvValues, remoteOverrideKeys), + yield* readAuthEmailTemplateContent( + yield* resolveAuthEmail( + config.auth.email, + authDocument, + projectEnvValues, + remoteOverrideKeys, + ), workdir, ); - const resolvedSmtp = resolveAuthEmailSmtp(authDocument, projectEnvValues, remoteOverrideKeys); + const resolvedSmtp = yield* resolveAuthEmailSmtp( + authDocument, + projectEnvValues, + remoteOverrideKeys, + ); const smtp: SmtpInput | undefined = resolvedSmtp === undefined ? undefined @@ -2743,7 +2890,7 @@ export function resolveLocalConfigValues( adminEmail: resolvedSmtp.adminEmail, }; - const thirdParty = resolveThirdPartyProviders( + const thirdParty = yield* resolveThirdPartyProviders( config.auth.third_party, projectEnvValues, remoteOverrideKeys, @@ -2764,7 +2911,7 @@ export function resolveLocalConfigValues( // gate, unlike `studio.port`/`local_smtp.port` above. const denoVersion = remoteWins("edge_runtime.deno_version") ? config.edge_runtime.deno_version - : envOverrideDenoVersion(config.edge_runtime.deno_version, projectEnvValues); + : yield* envOverrideDenoVersion(config.edge_runtime.deno_version, projectEnvValues); // When `analytics.enabled` and `analytics.backend == "bigquery"`, all three GCP fields are // required. Backend-enum validation is covered at decode time for the config.toml-sourced @@ -2772,34 +2919,34 @@ export function resolveLocalConfigValues( // env-override path — see {@link envOverrideAnalyticsBackend} for that case. const analyticsEnabled = remoteWins("analytics.enabled") ? config.analytics.enabled - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_ANALYTICS_ENABLED", config.analytics.enabled, "analytics.enabled", projectEnvValues, ); - const analyticsBackend = envOverrideAnalyticsBackend( + const analyticsBackend = yield* envOverrideAnalyticsBackend( config.analytics.backend, projectEnvValues, remoteWins("analytics.backend"), ); const gcpProjectId = remoteWins("analytics.gcp_project_id") ? config.analytics.gcp_project_id - : envOverride( + : yield* envOverride( "SUPABASE_ANALYTICS_GCP_PROJECT_ID", config.analytics.gcp_project_id, projectEnvValues, ); const gcpProjectNumber = remoteWins("analytics.gcp_project_number") ? config.analytics.gcp_project_number - : envOverride( + : yield* envOverride( "SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER", config.analytics.gcp_project_number, projectEnvValues, ); const gcpJwtPath = remoteWins("analytics.gcp_jwt_path") ? config.analytics.gcp_jwt_path - : envOverride( + : yield* envOverride( "SUPABASE_ANALYTICS_GCP_JWT_PATH", config.analytics.gcp_jwt_path, projectEnvValues, @@ -2816,7 +2963,7 @@ export function resolveLocalConfigValues( // `validateResolvedConfig`'s existing JSON-validity check reports it. const webhooksEnabled = remoteWins("experimental.webhooks.enabled") ? config.experimental.webhooks?.enabled === true - : envOverrideBool( + : yield* envOverrideBool( "SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED", config.experimental.webhooks?.enabled === true, "experimental.webhooks.enabled", @@ -2824,11 +2971,11 @@ export function resolveLocalConfigValues( ); const pgdeltaFormatOptions = remoteWins("experimental.pgdelta.format_options") ? (config.experimental.pgdelta?.format_options ?? "") - : (envOverride( + : ((yield* envOverride( "SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS", config.experimental.pgdelta?.format_options, projectEnvValues, - ) ?? ""); + )) ?? ""); // Every pure validation check runs in one place, here, rather than interleaved with this // function's 3 I/O reads (signing keys, api.tls cert/key, email content) at their original @@ -2872,12 +3019,20 @@ export function resolveLocalConfigValues( analytics: analyticsInput, experimental: experimentalInput, }; - validateResolvedConfig(input); + yield* Effect.try({ + try: () => validateResolvedConfig(input), + catch: (cause) => (cause instanceof ConfigValidateError ? cause : thrownError(cause)), + }); // sms/external run after the shared validation call, since both need the env-override-aware // resolved value rather than the raw decoded config it validates. if (authEnabled) { - validateAuthSmsProviders(authDocument, config.auth.sms, projectEnvValues, remoteOverrideKeys); - validateAuthExternalProviders( + yield* validateAuthSmsProviders( + authDocument, + config.auth.sms, + projectEnvValues, + remoteOverrideKeys, + ); + yield* validateAuthExternalProviders( authDocument, config.auth.external, projectEnvValues, @@ -2887,9 +3042,9 @@ export function resolveLocalConfigValues( // Decrypted like `auth.email.smtp.pass`/`auth.captcha.secret`. const openaiApiKey = remoteWins("studio.openai_api_key") - ? decryptAuthSecret(config.studio.openai_api_key, projectEnvValues) - : decryptAuthSecret( - envOverride( + ? yield* decryptAuthSecret(config.studio.openai_api_key, projectEnvValues) + : yield* decryptAuthSecret( + yield* envOverride( "SUPABASE_STUDIO_OPENAI_API_KEY", config.studio.openai_api_key, projectEnvValues, @@ -2925,9 +3080,9 @@ export function resolveLocalConfigValues( // Decrypted like `anon_key`/`service_role_key` below. publishableKey: resolveOpaqueKey( remoteWins("auth.publishable_key") - ? decryptAuthSecret(config.auth.publishable_key, projectEnvValues) - : decryptAuthSecret( - envOverride( + ? yield* decryptAuthSecret(config.auth.publishable_key, projectEnvValues) + : yield* decryptAuthSecret( + yield* envOverride( "SUPABASE_AUTH_PUBLISHABLE_KEY", config.auth.publishable_key, projectEnvValues, @@ -2938,30 +3093,34 @@ export function resolveLocalConfigValues( ), secretKey: resolveOpaqueKey( remoteWins("auth.secret_key") - ? decryptAuthSecret(config.auth.secret_key, projectEnvValues) - : decryptAuthSecret( - envOverride("SUPABASE_AUTH_SECRET_KEY", config.auth.secret_key, projectEnvValues), + ? yield* decryptAuthSecret(config.auth.secret_key, projectEnvValues) + : yield* decryptAuthSecret( + yield* envOverride( + "SUPABASE_AUTH_SECRET_KEY", + config.auth.secret_key, + projectEnvValues, + ), projectEnvValues, ), defaultSecretKey, ), jwtSecret, - anonKey: resolveSignedKey( - decryptAuthSecret( + anonKey: yield* resolveSignedKey( + yield* decryptAuthSecret( remoteWins("auth.anon_key") ? config.auth.anon_key - : envOverride("SUPABASE_AUTH_ANON_KEY", config.auth.anon_key, projectEnvValues), + : yield* envOverride("SUPABASE_AUTH_ANON_KEY", config.auth.anon_key, projectEnvValues), projectEnvValues, ), jwtSecret, signingKey, "anon", ), - serviceRoleKey: resolveSignedKey( - decryptAuthSecret( + serviceRoleKey: yield* resolveSignedKey( + yield* decryptAuthSecret( remoteWins("auth.service_role_key") ? config.auth.service_role_key - : envOverride( + : yield* envOverride( "SUPABASE_AUTH_SERVICE_ROLE_KEY", config.auth.service_role_key, projectEnvValues, @@ -2985,8 +3144,13 @@ export function resolveLocalConfigValues( // presence-only and must see the raw value to reject an explicit `project_id = ""`. projectId: sanitizeProjectId(resolvedProjectId ?? ""), edgeRuntimeDenoVersion: denoVersion, - }; -} + } satisfies LocalConfigValues; +}); + +const asConfigValidateError = (cause: unknown) => + cause instanceof ConfigValidateError + ? cause + : new ConfigValidateError(cause instanceof Error ? cause.message : String(cause)); /** Resolves local and remote signing keys for services that consume the local JWKS document. */ export const resolveLocalJwks = Effect.fnUntraced(function* ( @@ -2996,158 +3160,156 @@ export const resolveLocalJwks = Effect.fnUntraced(function* ( projectEnvValues?: Readonly>, remoteOverrideKeys: ReadonlySet = new Set(), ) { - const { issuerUrl, signingKeys, signingKeysPath } = yield* Effect.try({ - try: () => { - const remoteWins = makeRemoteWins(remoteOverrideKeys); - const signingKeysPath = remoteWins("auth.signing_keys_path") - ? config.auth.signing_keys_path - : envOverride( - "SUPABASE_AUTH_SIGNING_KEYS_PATH", - config.auth.signing_keys_path, - projectEnvValues, - ); - // Every resolved config carries the default ES256 key, regardless of `auth.enabled`. It's - // only ever replaced by a configured `signing_keys_path` file, and only when that file is - // actually read (gated on `auth.enabled` — see {@link resolveConfiguredSigningKeys}). So JWKS - // resolution always publishes either the file's keys or this default, never neither — - // `GOTRUE_JWT_KEYS` signs with the same default, so the two must never disagree. - const signingKeys: ReadonlyArray = resolveConfiguredSigningKeys( - config, - workdir, - projectEnvValues, - remoteOverrideKeys, - ) ?? [DEFAULT_SIGNING_KEY]; - - // Built as a `ThirdPartyProvidersLike` (every provider's full field set, including auth0's - // `tenant_region`) rather than the validation-only `ThirdPartyInput`, since - // {@link resolveThirdPartyIssuerUrl} needs the full set to build the issuer URL. - const thirdParty: ThirdPartyProvidersLike = { - firebase: { - enabled: remoteWins("auth.third_party.firebase.enabled") - ? config.auth.third_party.firebase.enabled - : envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", - config.auth.third_party.firebase.enabled, - "auth.third_party.firebase.enabled", - projectEnvValues, - ), - project_id: remoteWins("auth.third_party.firebase.project_id") - ? config.auth.third_party.firebase.project_id - : envOverride( - "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID", - config.auth.third_party.firebase.project_id, - projectEnvValues, - ), - }, - auth0: { - enabled: remoteWins("auth.third_party.auth0.enabled") - ? config.auth.third_party.auth0.enabled - : envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_ENABLED", - config.auth.third_party.auth0.enabled, - "auth.third_party.auth0.enabled", - projectEnvValues, - ), - tenant: remoteWins("auth.third_party.auth0.tenant") - ? config.auth.third_party.auth0.tenant - : envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT", - config.auth.third_party.auth0.tenant, - projectEnvValues, - ), - tenant_region: remoteWins("auth.third_party.auth0.tenant_region") - ? config.auth.third_party.auth0.tenant_region - : envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT_REGION", - config.auth.third_party.auth0.tenant_region, - projectEnvValues, - ), - }, - aws_cognito: { - enabled: remoteWins("auth.third_party.aws_cognito.enabled") - ? config.auth.third_party.aws_cognito.enabled - : envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_ENABLED", - config.auth.third_party.aws_cognito.enabled, - "auth.third_party.aws_cognito.enabled", - projectEnvValues, - ), - user_pool_id: remoteWins("auth.third_party.aws_cognito.user_pool_id") - ? config.auth.third_party.aws_cognito.user_pool_id - : envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_ID", - config.auth.third_party.aws_cognito.user_pool_id, - projectEnvValues, - ), - user_pool_region: remoteWins("auth.third_party.aws_cognito.user_pool_region") - ? config.auth.third_party.aws_cognito.user_pool_region - : envOverride( - "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_REGION", - config.auth.third_party.aws_cognito.user_pool_region, - projectEnvValues, - ), - }, - clerk: { - enabled: remoteWins("auth.third_party.clerk.enabled") - ? config.auth.third_party.clerk.enabled - : envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", - config.auth.third_party.clerk.enabled, - "auth.third_party.clerk.enabled", - projectEnvValues, - ), - domain: remoteWins("auth.third_party.clerk.domain") - ? config.auth.third_party.clerk.domain - : envOverride( - "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", - config.auth.third_party.clerk.domain, - projectEnvValues, - ), - }, - workos: { - enabled: remoteWins("auth.third_party.workos.enabled") - ? config.auth.third_party.workos.enabled - : envOverrideBool( - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", - config.auth.third_party.workos.enabled, - "auth.third_party.workos.enabled", - projectEnvValues, - ), - issuer_url: remoteWins("auth.third_party.workos.issuer_url") - ? config.auth.third_party.workos.issuer_url - : envOverride( - "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", - config.auth.third_party.workos.issuer_url, - projectEnvValues, - ), - }, - }; - - // This function is called unconditionally, regardless of `auth.enabled`, but - // `resolveThirdPartyIssuerUrl`'s "at most one enabled" + required-field checks are only - // meaningful while auth is enabled — `resolveLocalConfigValues` already ran the equivalent - // check in that case. When auth is disabled, that validation is (correctly) skipped, so this - // uses the unchecked, no-throw issuer-url builder instead. - const authEnabled = remoteWins("auth.enabled") - ? config.auth.enabled - : envOverrideBool( - "SUPABASE_AUTH_ENABLED", - config.auth.enabled, - "auth.enabled", - projectEnvValues, - ); - const issuerUrl = Option.fromNullishOr( - authEnabled - ? resolveThirdPartyIssuerUrl(thirdParty) - : thirdPartyIssuerUrlUnchecked(thirdParty), - ).pipe(Option.filter((value) => value.length > 0)); - return { issuerUrl, signingKeys, signingKeysPath }; - }, - catch: (cause) => - cause instanceof ConfigValidateError - ? cause - : new ConfigValidateError(cause instanceof Error ? cause.message : String(cause)), - }); + const { issuerUrl, signingKeys, signingKeysPath } = yield* Effect.gen(function* () { + const remoteWins = makeRemoteWins(remoteOverrideKeys); + const signingKeysPath = remoteWins("auth.signing_keys_path") + ? config.auth.signing_keys_path + : yield* envOverride( + "SUPABASE_AUTH_SIGNING_KEYS_PATH", + config.auth.signing_keys_path, + projectEnvValues, + ); + // Every resolved config carries the default ES256 key, regardless of `auth.enabled`. It's + // only ever replaced by a configured `signing_keys_path` file, and only when that file is + // actually read (gated on `auth.enabled` — see {@link resolveConfiguredSigningKeys}). So JWKS + // resolution always publishes either the file's keys or this default, never neither — + // `GOTRUE_JWT_KEYS` signs with the same default, so the two must never disagree. + const signingKeys: ReadonlyArray = (yield* resolveConfiguredSigningKeys( + config, + workdir, + projectEnvValues, + remoteOverrideKeys, + )) ?? [DEFAULT_SIGNING_KEY]; + + // Built as a `ThirdPartyProvidersLike` (every provider's full field set, including auth0's + // `tenant_region`) rather than the validation-only `ThirdPartyInput`, since + // {@link resolveThirdPartyIssuerUrl} needs the full set to build the issuer URL. + const thirdParty: ThirdPartyProvidersLike = { + firebase: { + enabled: remoteWins("auth.third_party.firebase.enabled") + ? config.auth.third_party.firebase.enabled + : yield* envOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", + config.auth.third_party.firebase.enabled, + "auth.third_party.firebase.enabled", + projectEnvValues, + ), + project_id: remoteWins("auth.third_party.firebase.project_id") + ? config.auth.third_party.firebase.project_id + : yield* envOverride( + "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID", + config.auth.third_party.firebase.project_id, + projectEnvValues, + ), + }, + auth0: { + enabled: remoteWins("auth.third_party.auth0.enabled") + ? config.auth.third_party.auth0.enabled + : yield* envOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_AUTH0_ENABLED", + config.auth.third_party.auth0.enabled, + "auth.third_party.auth0.enabled", + projectEnvValues, + ), + tenant: remoteWins("auth.third_party.auth0.tenant") + ? config.auth.third_party.auth0.tenant + : yield* envOverride( + "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT", + config.auth.third_party.auth0.tenant, + projectEnvValues, + ), + tenant_region: remoteWins("auth.third_party.auth0.tenant_region") + ? config.auth.third_party.auth0.tenant_region + : yield* envOverride( + "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT_REGION", + config.auth.third_party.auth0.tenant_region, + projectEnvValues, + ), + }, + aws_cognito: { + enabled: remoteWins("auth.third_party.aws_cognito.enabled") + ? config.auth.third_party.aws_cognito.enabled + : yield* envOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_ENABLED", + config.auth.third_party.aws_cognito.enabled, + "auth.third_party.aws_cognito.enabled", + projectEnvValues, + ), + user_pool_id: remoteWins("auth.third_party.aws_cognito.user_pool_id") + ? config.auth.third_party.aws_cognito.user_pool_id + : yield* envOverride( + "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_ID", + config.auth.third_party.aws_cognito.user_pool_id, + projectEnvValues, + ), + user_pool_region: remoteWins("auth.third_party.aws_cognito.user_pool_region") + ? config.auth.third_party.aws_cognito.user_pool_region + : yield* envOverride( + "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_REGION", + config.auth.third_party.aws_cognito.user_pool_region, + projectEnvValues, + ), + }, + clerk: { + enabled: remoteWins("auth.third_party.clerk.enabled") + ? config.auth.third_party.clerk.enabled + : yield* envOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", + config.auth.third_party.clerk.enabled, + "auth.third_party.clerk.enabled", + projectEnvValues, + ), + domain: remoteWins("auth.third_party.clerk.domain") + ? config.auth.third_party.clerk.domain + : yield* envOverride( + "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", + config.auth.third_party.clerk.domain, + projectEnvValues, + ), + }, + workos: { + enabled: remoteWins("auth.third_party.workos.enabled") + ? config.auth.third_party.workos.enabled + : yield* envOverrideBool( + "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", + config.auth.third_party.workos.enabled, + "auth.third_party.workos.enabled", + projectEnvValues, + ), + issuer_url: remoteWins("auth.third_party.workos.issuer_url") + ? config.auth.third_party.workos.issuer_url + : yield* envOverride( + "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", + config.auth.third_party.workos.issuer_url, + projectEnvValues, + ), + }, + }; + + // This function is called unconditionally, regardless of `auth.enabled`, but + // `resolveThirdPartyIssuerUrl`'s "at most one enabled" + required-field checks are only + // meaningful while auth is enabled — `resolveLocalConfigValues` already ran the equivalent + // check in that case. When auth is disabled, that validation is (correctly) skipped, so this + // uses the unchecked, no-throw issuer-url builder instead. + const authEnabled = remoteWins("auth.enabled") + ? config.auth.enabled + : yield* envOverrideBool( + "SUPABASE_AUTH_ENABLED", + config.auth.enabled, + "auth.enabled", + projectEnvValues, + ); + const issuerUrl = Option.fromNullishOr( + yield* Effect.try({ + try: () => + authEnabled + ? resolveThirdPartyIssuerUrl(thirdParty) + : thirdPartyIssuerUrlUnchecked(thirdParty), + catch: asConfigValidateError, + }), + ).pipe(Option.filter((value) => value.length > 0)); + return { issuerUrl, signingKeys, signingKeysPath }; + }).pipe(Effect.mapError(asConfigValidateError)); const keys: unknown[] = []; if (Option.isSome(issuerUrl)) { keys.push( diff --git a/apps/cli/src/command-internal/local-config-values.unit.test.ts b/apps/cli/src/command-internal/local-config-values.unit.test.ts index 75fcdbb769..126270f897 100644 --- a/apps/cli/src/command-internal/local-config-values.unit.test.ts +++ b/apps/cli/src/command-internal/local-config-values.unit.test.ts @@ -1,14 +1,13 @@ import { generateKeyPairSync } from "node:crypto"; -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { BunServices } from "@effect/platform-bun"; +import { afterEach, describe, expect, it, vi } from "@effect/vitest"; import { CliConfigSchema, type CliConfig } from "@supabase/config"; -import { Effect, Schema } from "effect"; +import { ConfigProvider, Effect, FileSystem, Layer, Path, Schema } from "effect"; import { FetchHttpClient } from "effect/http"; import { importJWK, jwtVerify } from "jose"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { useTempWorkdir } from "../../tests/helpers/command-mocks.ts"; +import { useTempWorkdir, withEnvVar } from "../../tests/helpers/command-mocks.ts"; import { DEFAULT_SIGNING_KEY } from "./go-jwt.ts"; import { POSTGRES_DEFAULT_ROOT_KEY, @@ -43,14 +42,31 @@ import { resolveLocalJwks, } from "./local-config-values.ts"; +/** The `SUPABASE_*` overrides a test pins: the only env its `Config` reads see. */ +const testEnv: Record = {}; + +const testLayer = Layer.mergeAll( + BunServices.layer, + Layer.succeed( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnvRecord(testEnv, { preserveEmptyStrings: true }), + ), +); + +const run = (effect: Effect.Effect) => + Effect.runSync( + withEnvVar("DOTENV_PRIVATE_KEY", undefined, effect).pipe(Effect.provide(testLayer)), + ); + const runLocalJwks = (...args: Parameters) => - Effect.runPromise( - resolveLocalJwks(...args).pipe( - Effect.provide(FetchHttpClient.layer), - Effect.provideService(FetchHttpClient.Fetch, globalThis.fetch), - ), + resolveLocalJwks(...args).pipe( + Effect.provide(FetchHttpClient.layer), + Effect.provideService(FetchHttpClient.Fetch, globalThis.fetch), ); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); +const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); + const decodeConfig = Schema.decodeUnknownSync(CliConfigSchema); const WORKDIR = "/tmp/local-config-values-test"; @@ -65,16 +81,21 @@ function generateRsaJwk(): Record { return { ...jwk, alg: "RS256", kid: "test-rsa-kid" }; } -function writeSigningKeys(workdir: string, jwks: ReadonlyArray>) { - const supabaseDir = join(workdir, "supabase"); - mkdirSync(supabaseDir, { recursive: true }); - writeFileSync(join(supabaseDir, "signing_keys.json"), JSON.stringify(jwks)); -} +const writeSigningKeys = Effect.fnUntraced(function* ( + workdir: string, + jwks: ReadonlyArray>, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const supabaseDir = path.join(workdir, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString(path.join(supabaseDir, "signing_keys.json"), yield* encodeJson(jwks)); +}); describe("resolveLocalConfigValues", () => { it("derives every URL from api.external_url when unset", () => { const config = baseConfig(); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("http://127.0.0.1:54321"); expect(values.restUrl).toBe("http://127.0.0.1:54321/rest/v1"); @@ -88,32 +109,32 @@ describe("resolveLocalConfigValues", () => { it("uses https and the configured port when api.tls.enabled", () => { const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("https://127.0.0.1:54321"); }); it("uses api.external_url verbatim when configured", () => { const config = baseConfig({ api: { external_url: "https://example.test" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("https://example.test"); expect(values.restUrl).toBe("https://example.test/rest/v1"); }); it("brackets an IPv6 hostname when building host:port", () => { const config = baseConfig(); - const values = resolveLocalConfigValues(config, "::1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "::1", WORKDIR)); expect(values.apiUrl).toBe("http://[::1]:54321"); }); it("builds the db URL with the hardcoded postgres password", () => { const config = baseConfig({ db: { port: 54322 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); }); it("falls back to the default JWT secret and opaque keys when unset", () => { const config = baseConfig(); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.jwtSecret).toBe("super-secret-jwt-token-with-at-least-32-characters-long"); expect(values.publishableKey).toBe("sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH"); expect(values.secretKey).toBe("sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz"); @@ -123,14 +144,14 @@ describe("resolveLocalConfigValues", () => { const config = baseConfig({ auth: { publishable_key: "sb_publishable_custom", secret_key: "sb_secret_custom" }, }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.publishableKey).toBe("sb_publishable_custom"); expect(values.secretKey).toBe("sb_secret_custom"); }); it("signs the default anon/service_role JWTs from the resolved secret", () => { const config = baseConfig(); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); const [, anonPayload] = values.anonKey.split("."); const [, serviceRolePayload] = values.serviceRoleKey.split("."); expect(JSON.parse(Buffer.from(anonPayload ?? "", "base64url").toString())).toMatchObject({ @@ -145,21 +166,21 @@ describe("resolveLocalConfigValues", () => { const config = baseConfig({ auth: { anon_key: "configured-anon", service_role_key: "configured-service-role" }, }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.anonKey).toBe("configured-anon"); expect(values.serviceRoleKey).toBe("configured-service-role"); }); it("signs anon/service_role JWTs from a configured jwt_secret", () => { const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.jwtSecret).toBe("a".repeat(32)); expect(values.anonKey).not.toBe(""); }); it("rejects a configured jwt_secret shorter than 16 characters", () => { const config = baseConfig({ auth: { jwt_secret: "a".repeat(15) } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidJwtSecretError, ); }); @@ -170,54 +191,51 @@ describe("resolveLocalConfigValues", () => { const VAULT_ENCRYPTED = "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/"; - afterEach(() => { - delete process.env["DOTENV_PRIVATE_KEY"]; - }); + const withVaultKey = (effect: Effect.Effect) => + withEnvVar("DOTENV_PRIVATE_KEY", VAULT_PRIVATE_KEY, effect); it("decrypts an encrypted: jwt_secret when DOTENV_PRIVATE_KEY is set", () => { - process.env["DOTENV_PRIVATE_KEY"] = VAULT_PRIVATE_KEY; const config = baseConfig({ auth: { jwt_secret: VAULT_ENCRYPTED } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( - InvalidJwtSecretError, - ); + expect(() => + run(withVaultKey(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))), + ).toThrow(InvalidJwtSecretError); }); it("decrypts an encrypted: publishable_key when DOTENV_PRIVATE_KEY is set", () => { - process.env["DOTENV_PRIVATE_KEY"] = VAULT_PRIVATE_KEY; const config = baseConfig({ auth: { publishable_key: VAULT_ENCRYPTED } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(withVaultKey(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))); expect(values.publishableKey).toBe("value"); }); it("fails config loading for an encrypted: secret with no private key, matching Go", () => { const config = baseConfig({ auth: { publishable_key: VAULT_ENCRYPTED } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "failed to parse config: missing private key", ); }); it("decrypts an encrypted: auth.email.smtp.pass, matching Go's Secret-typed Smtp.Pass field", () => { - process.env["DOTENV_PRIVATE_KEY"] = VAULT_PRIVATE_KEY; const document = { auth: { email: { smtp: { enabled: true, pass: VAULT_ENCRYPTED } } } }; - const resolved = resolveAuthEmailSmtp(document.auth, undefined); + const resolved = run(withVaultKey(resolveAuthEmailSmtp(document.auth, undefined))); expect(resolved?.pass).toBe("value"); }); it("decrypts an encrypted: studio.openai_api_key, matching Go's Secret-typed OpenaiApiKey field", () => { - process.env["DOTENV_PRIVATE_KEY"] = VAULT_PRIVATE_KEY; const config = baseConfig({ studio: { openai_api_key: VAULT_ENCRYPTED } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(withVaultKey(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))); expect(values.openaiApiKey).toBe("value"); }); it("decrypts an encrypted: SUPABASE_AUTH_* env override, not just the config.toml value", () => { - process.env["DOTENV_PRIVATE_KEY"] = VAULT_PRIVATE_KEY; const config = baseConfig(); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, { - SUPABASE_AUTH_SECRET_KEY: VAULT_ENCRYPTED, - }); + const values = run( + withVaultKey( + resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, { + SUPABASE_AUTH_SECRET_KEY: VAULT_ENCRYPTED, + }), + ), + ); expect(values.secretKey).toBe("value"); - delete process.env["DOTENV_PRIVATE_KEY"]; }); }); @@ -225,46 +243,50 @@ describe("resolveLocalConfigValues", () => { // An explicit `project_id = ""` overwrites the workdir-basename default with the literal // empty string, unlike an absent key. const config = baseConfig({ project_id: "" }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: project_id", ); }); it("does not reject an absent project_id when the workdir basename sanitizes to a non-empty value", () => { - const config = Schema.decodeUnknownSync(CliConfigSchema)({}); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + const config = Schema.decodeSync(CliConfigSchema)({}); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("rejects an absent project_id when the workdir basename sanitizes to empty, matching Go", () => { // The workdir-basename default still applies with no `project_id` key present, so a workdir // whose basename sanitizes to empty (e.g. `!!!`) still fails validation. - const config = Schema.decodeUnknownSync(CliConfigSchema)({}); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", "/tmp/!!!")).toThrow( + const config = Schema.decodeSync(CliConfigSchema)({}); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", "/tmp/!!!"))).toThrow( "Missing required field in config: project_id", ); }); it("lets SUPABASE_PROJECT_ID override an absent project_id whose basename sanitizes to empty", () => { - const config = Schema.decodeUnknownSync(CliConfigSchema)({}); + const config = Schema.decodeSync(CliConfigSchema)({}); expect(() => - resolveLocalConfigValues(config, "127.0.0.1", "/tmp/!!!", { - SUPABASE_PROJECT_ID: "env-project", - }), + run( + resolveLocalConfigValues(config, "127.0.0.1", "/tmp/!!!", { + SUPABASE_PROJECT_ID: "env-project", + }), + ), ).not.toThrow(); }); it("lets SUPABASE_PROJECT_ID override an explicit empty project_id", () => { const config = baseConfig({ project_id: "" }); expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, { - SUPABASE_PROJECT_ID: "env-project", - }), + run( + resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, { + SUPABASE_PROJECT_ID: "env-project", + }), + ), ).not.toThrow(); }); it("hardcodes the Go-parity local S3 credentials", () => { const config = baseConfig(); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.storageS3AccessKeyId).toBe("625729a08b95bf1b7ff351a663f3a23c"); expect(values.storageS3SecretAccessKey).toBe( "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", @@ -285,80 +307,91 @@ describe("resolveLocalConfigValues", () => { ] as const; afterEach(() => { - for (const key of ENV_KEYS) delete process.env[key]; + for (const key of ENV_KEYS) delete testEnv[key]; }); it("overrides jwt_secret even when config.toml sets one", () => { - process.env["SUPABASE_AUTH_JWT_SECRET"] = "b".repeat(32); + testEnv["SUPABASE_AUTH_JWT_SECRET"] = "b".repeat(32); const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.jwtSecret).toBe("b".repeat(32)); }); it("overrides publishable_key/secret_key", () => { - process.env["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "env-publishable"; - process.env["SUPABASE_AUTH_SECRET_KEY"] = "env-secret"; + testEnv["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "env-publishable"; + testEnv["SUPABASE_AUTH_SECRET_KEY"] = "env-secret"; const config = baseConfig({ auth: { publishable_key: "config-publishable", secret_key: "config-secret" }, }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.publishableKey).toBe("env-publishable"); expect(values.secretKey).toBe("env-secret"); }); it("overrides anon_key/service_role_key", () => { - process.env["SUPABASE_AUTH_ANON_KEY"] = "env-anon"; - process.env["SUPABASE_AUTH_SERVICE_ROLE_KEY"] = "env-service-role"; + testEnv["SUPABASE_AUTH_ANON_KEY"] = "env-anon"; + testEnv["SUPABASE_AUTH_SERVICE_ROLE_KEY"] = "env-service-role"; const config = baseConfig({ auth: { anon_key: "config-anon", service_role_key: "config-service-role" }, }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.anonKey).toBe("env-anon"); expect(values.serviceRoleKey).toBe("env-service-role"); }); it("treats an empty env var as unset, matching Viper's default", () => { - process.env["SUPABASE_AUTH_JWT_SECRET"] = ""; + testEnv["SUPABASE_AUTH_JWT_SECRET"] = ""; const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.jwtSecret).toBe("a".repeat(32)); }); it("still applies the short-secret validation to an env-provided jwt_secret", () => { - process.env["SUPABASE_AUTH_JWT_SECRET"] = "too-short"; + testEnv["SUPABASE_AUTH_JWT_SECRET"] = "too-short"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidJwtSecretError, ); }); - it("overrides signing_keys_path even when config.toml doesn't set one", async () => { - const jwk = generateRsaJwk(); - writeSigningKeys(tempRoot.current, [jwk]); - process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "signing_keys.json"; - const config = baseConfig(); - const values = resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - - const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; - const publicKey = await importJWK(publicJwk, "RS256"); - const { protectedHeader } = await jwtVerify(values.anonKey, publicKey); - expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); - }); - - it("prefers an env-provided signing_keys_path over config.toml's", () => { - const envJwk = { ...generateRsaJwk(), kid: "env-kid" }; - const configJwk = { ...generateRsaJwk(), kid: "config-kid" }; - writeSigningKeys(tempRoot.current, [envJwk]); - const supabaseDir = join(tempRoot.current, "supabase"); - writeFileSync(join(supabaseDir, "other_keys.json"), JSON.stringify([configJwk])); - process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "signing_keys.json"; - const config = baseConfig({ auth: { signing_keys_path: "other_keys.json" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - const [header] = values.anonKey.split("."); - expect(JSON.parse(Buffer.from(header ?? "", "base64url").toString())).toMatchObject({ - kid: "env-kid", - }); - }); + it.live("overrides signing_keys_path even when config.toml doesn't set one", () => + Effect.gen(function* () { + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "signing_keys.json"; + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + + const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; + const publicKey = yield* Effect.promise(() => importJWK(publicJwk, "RS256")); + const { protectedHeader } = yield* Effect.promise(() => + jwtVerify(values.anonKey, publicKey), + ); + expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); + }).pipe(Effect.provide(testLayer)), + ); + + it.live("prefers an env-provided signing_keys_path over config.toml's", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const envJwk = { ...generateRsaJwk(), kid: "env-kid" }; + const configJwk = { ...generateRsaJwk(), kid: "config-kid" }; + yield* writeSigningKeys(tempRoot.current, [envJwk]); + const supabaseDir = path.join(tempRoot.current, "supabase"); + yield* fs.writeFileString( + path.join(supabaseDir, "other_keys.json"), + yield* encodeJson([configJwk]), + ); + testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "signing_keys.json"; + const config = baseConfig({ auth: { signing_keys_path: "other_keys.json" } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + const [header] = values.anonKey.split("."); + expect(yield* decodeJson(Buffer.from(header ?? "", "base64url").toString())).toMatchObject({ + kid: "env-kid", + }); + }).pipe(Effect.provide(testLayer)), + ); }); describe("SUPABASE_* env(VAR) indirection (Go's LoadEnvHook)", () => { @@ -367,41 +400,41 @@ describe("resolveLocalConfigValues", () => { const ENV_KEYS = ["SUPABASE_AUTH_JWT_SECRET", "SUPABASE_DB_PORT", "SUPABASE_API_ENABLED"]; afterEach(() => { - for (const key of ENV_KEYS) delete process.env[key]; - delete process.env["INDIRECT_JWT_SECRET"]; - delete process.env["INDIRECT_DB_PORT"]; - delete process.env["INDIRECT_API_ENABLED"]; + for (const key of ENV_KEYS) delete testEnv[key]; + delete testEnv["INDIRECT_JWT_SECRET"]; + delete testEnv["INDIRECT_DB_PORT"]; + delete testEnv["INDIRECT_API_ENABLED"]; }); it("resolves a string override's env(VAR) indirection", () => { - process.env["SUPABASE_AUTH_JWT_SECRET"] = "env(INDIRECT_JWT_SECRET)"; - process.env["INDIRECT_JWT_SECRET"] = "c".repeat(32); + testEnv["SUPABASE_AUTH_JWT_SECRET"] = "env(INDIRECT_JWT_SECRET)"; + testEnv["INDIRECT_JWT_SECRET"] = "c".repeat(32); const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.jwtSecret).toBe("c".repeat(32)); }); it("resolves a port override's env(VAR) indirection", () => { - process.env["SUPABASE_DB_PORT"] = "env(INDIRECT_DB_PORT)"; - process.env["INDIRECT_DB_PORT"] = "54329"; + testEnv["SUPABASE_DB_PORT"] = "env(INDIRECT_DB_PORT)"; + testEnv["INDIRECT_DB_PORT"] = "54329"; const config = baseConfig({ db: { port: 54322 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54329/postgres"); }); it("resolves a bool override's env(VAR) indirection", () => { - process.env["SUPABASE_API_ENABLED"] = "env(INDIRECT_API_ENABLED)"; - process.env["INDIRECT_API_ENABLED"] = "false"; + testEnv["SUPABASE_API_ENABLED"] = "env(INDIRECT_API_ENABLED)"; + testEnv["INDIRECT_API_ENABLED"] = "false"; const config = baseConfig({ api: { enabled: true, tls: { enabled: true, cert_path: "missing-cert.pem" } }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("preserves the env(VAR) literal when the indirected var is unset, matching Go", () => { - process.env["SUPABASE_AUTH_JWT_SECRET"] = "env(INDIRECT_JWT_SECRET)"; + testEnv["SUPABASE_AUTH_JWT_SECRET"] = "env(INDIRECT_JWT_SECRET)"; const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.jwtSecret).toBe("env(INDIRECT_JWT_SECRET)"); }); }); @@ -417,50 +450,50 @@ describe("resolveLocalConfigValues", () => { ] as const; afterEach(() => { - for (const key of ENV_KEYS) delete process.env[key]; + for (const key of ENV_KEYS) delete testEnv[key]; }); it("overrides db.port for the derived DB URL and the exposed dbPort", () => { - process.env["SUPABASE_DB_PORT"] = "54329"; + testEnv["SUPABASE_DB_PORT"] = "54329"; const config = baseConfig({ db: { port: 54322 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54329/postgres"); expect(values.dbPort).toBe(54329); }); it("overrides studio.port for the derived Studio URL", () => { - process.env["SUPABASE_STUDIO_PORT"] = "54330"; + testEnv["SUPABASE_STUDIO_PORT"] = "54330"; const config = baseConfig({ studio: { port: 54323 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.studioUrl).toBe("http://127.0.0.1:54330"); }); it("overrides local_smtp.port for the derived Mailpit URL", () => { - process.env["SUPABASE_LOCAL_SMTP_PORT"] = "54331"; + testEnv["SUPABASE_LOCAL_SMTP_PORT"] = "54331"; const config = baseConfig({ local_smtp: { port: 54324 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.mailpitUrl).toBe("http://127.0.0.1:54331"); }); it("overrides api.port for every API-derived URL", () => { - process.env["SUPABASE_API_PORT"] = "54332"; + testEnv["SUPABASE_API_PORT"] = "54332"; const config = baseConfig({ api: { port: 54321 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("http://127.0.0.1:54332"); expect(values.restUrl).toBe("http://127.0.0.1:54332/rest/v1"); }); it("overrides api.external_url even when config.toml sets one", () => { - process.env["SUPABASE_API_EXTERNAL_URL"] = "https://env-override.example"; + testEnv["SUPABASE_API_EXTERNAL_URL"] = "https://env-override.example"; const config = baseConfig({ api: { external_url: "https://config.example" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("https://env-override.example"); }); it("treats an empty non-auth env var as unset, matching Viper's default", () => { - process.env["SUPABASE_DB_PORT"] = ""; + testEnv["SUPABASE_DB_PORT"] = ""; const config = baseConfig({ db: { port: 54322 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); }); @@ -470,47 +503,47 @@ describe("resolveLocalConfigValues", () => { "SUPABASE_LOCAL_SMTP_PORT", "SUPABASE_API_PORT", ] as const)("rejects a malformed %s override instead of producing NaN", (envKey) => { - process.env[envKey] = "abc"; + testEnv[envKey] = "abc"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidPortEnvOverrideError, ); }); it("rejects a SUPABASE_DB_PORT override above the uint16 range", () => { - process.env["SUPABASE_DB_PORT"] = "99999"; + testEnv["SUPABASE_DB_PORT"] = "99999"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidPortEnvOverrideError, ); }); it("resolves an octal leading-zero SUPABASE_DB_PORT override to its octal value, not decimal", () => { - process.env["SUPABASE_DB_PORT"] = "010"; + testEnv["SUPABASE_DB_PORT"] = "010"; const config = baseConfig({ db: { port: 54322 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.dbPort).toBe(8); expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:8/postgres"); }); it("resolves a 0x-prefixed SUPABASE_DB_PORT override as hex", () => { - process.env["SUPABASE_DB_PORT"] = "0x1F90"; + testEnv["SUPABASE_DB_PORT"] = "0x1F90"; const config = baseConfig({ db: { port: 54322 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.dbPort).toBe(8080); }); it("still resolves a plain decimal SUPABASE_DB_PORT override with no leading zero", () => { - process.env["SUPABASE_DB_PORT"] = "5432"; + testEnv["SUPABASE_DB_PORT"] = "5432"; const config = baseConfig({ db: { port: 54322 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.dbPort).toBe(5432); }); it("rejects a 0x-prefixed SUPABASE_DB_PORT override exceeding the uint16 range", () => { - process.env["SUPABASE_DB_PORT"] = "0x1FFFF"; + testEnv["SUPABASE_DB_PORT"] = "0x1FFFF"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidPortEnvOverrideError, ); }); @@ -518,9 +551,9 @@ describe("resolveLocalConfigValues", () => { // Unlike the malformed/out-of-range cases above, db.port=0 is a required-field failure with // no `enabled` gate, unlike api.port/studio.port/local_smtp.port. it("rejects a zero SUPABASE_DB_PORT override, matching Go's required-field check", () => { - process.env["SUPABASE_DB_PORT"] = "0"; + testEnv["SUPABASE_DB_PORT"] = "0"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: db.port", ); }); @@ -529,67 +562,67 @@ describe("resolveLocalConfigValues", () => { // `api.enabled = true`. it("rejects a configured api.port of 0 when api is enabled", () => { const config = baseConfig({ api: { port: 0 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: api.port", ); }); it("rejects a zero SUPABASE_API_PORT override when api is enabled", () => { - process.env["SUPABASE_API_PORT"] = "0"; + testEnv["SUPABASE_API_PORT"] = "0"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: api.port", ); }); it("does not reject a zero api.port when api is disabled", () => { const config = baseConfig({ api: { enabled: false, port: 0 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); // studio.enabled defaults to true, so this rejection applies without an explicit // `studio.enabled = true`. it("rejects a configured studio.port of 0 when studio is enabled", () => { const config = baseConfig({ studio: { port: 0 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: studio.port", ); }); it("rejects a zero SUPABASE_STUDIO_PORT override when studio is enabled", () => { - process.env["SUPABASE_STUDIO_PORT"] = "0"; + testEnv["SUPABASE_STUDIO_PORT"] = "0"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: studio.port", ); }); it("does not reject a zero studio.port when studio is disabled", () => { const config = baseConfig({ studio: { enabled: false, port: 0 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("rejects a malformed studio.api_url (unterminated IPv6 literal) when studio is enabled", () => { const config = baseConfig({ studio: { api_url: "http://[::1" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, ); }); it("does not reject a malformed studio.api_url when studio is disabled", () => { const config = baseConfig({ studio: { enabled: false, api_url: "http://[::1" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("does not throw for the default studio.api_url", () => { const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("rejects a malformed SUPABASE_STUDIO_API_URL override", () => { - process.env["SUPABASE_STUDIO_API_URL"] = "http://[::1"; + testEnv["SUPABASE_STUDIO_API_URL"] = "http://[::1"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, ); }); @@ -598,22 +631,22 @@ describe("resolveLocalConfigValues", () => { // `local_smtp.enabled = true`. it("rejects a configured local_smtp.port of 0 when local_smtp is enabled", () => { const config = baseConfig({ local_smtp: { port: 0 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: local_smtp.port", ); }); it("rejects a zero SUPABASE_LOCAL_SMTP_PORT override when local_smtp is enabled", () => { - process.env["SUPABASE_LOCAL_SMTP_PORT"] = "0"; + testEnv["SUPABASE_LOCAL_SMTP_PORT"] = "0"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: local_smtp.port", ); }); it("does not reject a zero local_smtp.port when local_smtp is disabled", () => { const config = baseConfig({ local_smtp: { enabled: false, port: 0 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); @@ -621,35 +654,35 @@ describe("resolveLocalConfigValues", () => { // Required-field/range assertions live in config-validate.unit.test.ts; only env-override // mechanics are tested here. afterEach(() => { - delete process.env["SUPABASE_DB_MAJOR_VERSION"]; + delete testEnv["SUPABASE_DB_MAJOR_VERSION"]; }); it("overrides a valid configured major_version via SUPABASE_DB_MAJOR_VERSION", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "15"; + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "15"; const config = baseConfig({ db: { major_version: 17 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("rejects an unsupported SUPABASE_DB_MAJOR_VERSION override", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "16"; + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "16"; const config = baseConfig({ db: { major_version: 17 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Failed reading config: Invalid db.major_version: 16.", ); }); it("rejects a non-numeric SUPABASE_DB_MAJOR_VERSION override", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "abc"; + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "abc"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Failed reading config: Invalid db.major_version: abc.", ); }); it("treats an empty SUPABASE_DB_MAJOR_VERSION override as unset, matching Viper's default", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = ""; + testEnv["SUPABASE_DB_MAJOR_VERSION"] = ""; const config = baseConfig({ db: { major_version: 17 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); @@ -657,22 +690,22 @@ describe("resolveLocalConfigValues", () => { // Always validated eagerly, with no presence/enabled gate — same bucket as // db.port/db.major_version above. afterEach(() => { - delete process.env["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"]; + delete testEnv["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"]; }); it("does not throw for a valid SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED override", () => { - process.env["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"] = "true"; + testEnv["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("rejects a malformed SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED override", () => { - process.env["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"] = "notabool"; + testEnv["SUPABASE_DB_NETWORK_RESTRICTIONS_ENABLED"] = "notabool"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidBoolEnvOverrideError, ); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'Invalid config for db.network_restrictions.enabled: cannot parse "notabool" as a bool', ); }); @@ -681,14 +714,16 @@ describe("resolveLocalConfigValues", () => { describe("db.root_key (unmodeled raw-document field)", () => { it("falls back to the default root key when absent", () => { const config = baseConfig(); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.rootKey).toBe(POSTGRES_DEFAULT_ROOT_KEY); }); it("uses a configured string root_key verbatim", () => { const config = baseConfig(); const document = { db: { root_key: "custom-root-key" } }; - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document); + const values = run( + resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + ); expect(values.rootKey).toBe("custom-root-key"); }); @@ -696,7 +731,7 @@ describe("resolveLocalConfigValues", () => { const config = baseConfig(); const document = { db: { root_key: 12345 } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow( "failed to parse config: decoding failed due to the following error(s):\n\n'db.root_key' expected a map or struct", ); @@ -711,37 +746,37 @@ describe("resolveLocalConfigValues", () => { // Required-field/range assertions live in config-validate.unit.test.ts; only env-override // mechanics are tested here. afterEach(() => { - delete process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"]; + delete testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"]; }); it("rejects a zero SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { - process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "0"; + testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "0"; const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: edge_runtime.deno_version", ); }); it("rejects an unsupported SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { - process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "3"; + testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "3"; const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Failed reading config: Invalid edge_runtime.deno_version: 3.", ); }); it("rejects a non-numeric SUPABASE_EDGE_RUNTIME_DENO_VERSION override", () => { - process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; + testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Failed reading config: Invalid edge_runtime.deno_version: abc.", ); }); it("treats an empty SUPABASE_EDGE_RUNTIME_DENO_VERSION override as unset, matching Viper's default", () => { - process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = ""; + testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = ""; const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); @@ -749,44 +784,44 @@ describe("resolveLocalConfigValues", () => { // Required-field/range assertions live in config-validate.unit.test.ts; only env-override // mechanics are tested here. afterEach(() => { - delete process.env["SUPABASE_ANALYTICS_ENABLED"]; - delete process.env["SUPABASE_ANALYTICS_BACKEND"]; - delete process.env["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; - delete process.env["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; - delete process.env["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; + delete testEnv["SUPABASE_ANALYTICS_ENABLED"]; + delete testEnv["SUPABASE_ANALYTICS_BACKEND"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; }); it("rejects a bigquery backend enabled only via SUPABASE_ANALYTICS_ENABLED", () => { - process.env["SUPABASE_ANALYTICS_ENABLED"] = "true"; + testEnv["SUPABASE_ANALYTICS_ENABLED"] = "true"; const config = baseConfig({ analytics: { enabled: false, backend: "bigquery" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: analytics.gcp_project_id", ); }); it("rejects a bigquery backend selected only via SUPABASE_ANALYTICS_BACKEND", () => { - process.env["SUPABASE_ANALYTICS_BACKEND"] = "bigquery"; + testEnv["SUPABASE_ANALYTICS_BACKEND"] = "bigquery"; const config = baseConfig({ analytics: { enabled: true, backend: "postgres" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: analytics.gcp_project_id", ); }); it("accepts env-provided GCP fields overriding empty config.toml values", () => { - process.env["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "proj"; - process.env["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "123"; - process.env["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "gcp.json"; + testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "proj"; + testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "123"; + testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "gcp.json"; const config = baseConfig({ analytics: { enabled: true, backend: "bigquery" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("rejects an invalid SUPABASE_ANALYTICS_BACKEND override", () => { - process.env["SUPABASE_ANALYTICS_BACKEND"] = "mysql"; + testEnv["SUPABASE_ANALYTICS_BACKEND"] = "mysql"; const config = baseConfig({ analytics: { enabled: true, backend: "postgres" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidAnalyticsBackendEnvOverrideError, ); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'Invalid config for analytics.backend: cannot parse "mysql" as one of "postgres", "bigquery"', ); }); @@ -797,66 +832,68 @@ describe("resolveLocalConfigValues", () => { // required-field/enabled checks themselves live in config-validate.unit.test.ts. it("does not throw a present [experimental.webhooks] section without enabled when no document is provided", () => { const config = baseConfig({ experimental: { webhooks: {} } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); // Required-field/range assertions live in config-validate.unit.test.ts; only env-override // mechanics are tested here. afterEach(() => { - delete process.env["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"]; - delete process.env["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"]; + delete testEnv["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"]; + delete testEnv["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"]; }); it("enables webhooks purely via SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED when the section omits enabled", () => { - process.env["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "true"; + testEnv["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "true"; const config = baseConfig({ experimental: { webhooks: {} } }); const document = { experimental: { webhooks: {} } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("rejects a malformed SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED override on an already-enabled section", () => { - process.env["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "notabool"; + testEnv["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "notabool"; const config = baseConfig({ experimental: { webhooks: { enabled: true } } }); const document = { experimental: { webhooks: { enabled: true } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow(InvalidBoolEnvOverrideError); expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow( 'Invalid config for experimental.webhooks.enabled: cannot parse "notabool" as a bool', ); }); it("rejects an invalid JSON SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS override", () => { - process.env["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = "{not valid json"; + testEnv["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = "{not valid json"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Invalid config for experimental.pgdelta.format_options: must be valid JSON", ); }); it("accepts a valid JSON SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS override", () => { - process.env["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = '{"keywordCase":"upper"}'; + testEnv["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = '{"keywordCase":"upper"}'; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("suppresses a malformed SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS when a remote block already set experimental.pgdelta.format_options (review: PRRT_kwDOErm0O86XLe6o)", () => { - process.env["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = "{not valid json"; + testEnv["SUPABASE_EXPERIMENTAL_PGDELTA_FORMAT_OPTIONS"] = "{not valid json"; const config = baseConfig({ experimental: { pgdelta: { format_options: '{"keywordCase":"upper"}' } }, }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["experimental.pgdelta.format_options"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["experimental.pgdelta.format_options"]), + ), ), ).not.toThrow(); }); @@ -866,66 +903,66 @@ describe("resolveLocalConfigValues", () => { // Applied before the default `api.external_url` scheme is derived, so it can flip http/https // even when config.toml disagrees. afterEach(() => { - delete process.env["SUPABASE_API_TLS_ENABLED"]; + delete testEnv["SUPABASE_API_TLS_ENABLED"]; }); it("overrides api.tls.enabled from false to true", () => { - process.env["SUPABASE_API_TLS_ENABLED"] = "true"; + testEnv["SUPABASE_API_TLS_ENABLED"] = "true"; const config = baseConfig({ api: { tls: { enabled: false }, port: 54321 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("https://127.0.0.1:54321"); }); it("overrides api.tls.enabled from true to false", () => { - process.env["SUPABASE_API_TLS_ENABLED"] = "false"; + testEnv["SUPABASE_API_TLS_ENABLED"] = "false"; const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("http://127.0.0.1:54321"); }); it("does not override api.tls.enabled once api.external_url is set", () => { - process.env["SUPABASE_API_TLS_ENABLED"] = "true"; + testEnv["SUPABASE_API_TLS_ENABLED"] = "true"; const config = baseConfig({ api: { external_url: "http://config.example" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("http://config.example"); }); it("rejects a malformed override instead of falling back to the configured value", () => { - process.env["SUPABASE_API_TLS_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_API_TLS_ENABLED"] = "not-a-bool"; const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidBoolEnvOverrideError, ); }); it("treats an empty override as unset, matching Viper's default", () => { - process.env["SUPABASE_API_TLS_ENABLED"] = ""; + testEnv["SUPABASE_API_TLS_ENABLED"] = ""; const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.apiUrl).toBe("https://127.0.0.1:54321"); }); }); describe("envOverrideRealtimeIpVersion", () => { afterEach(() => { - delete process.env["SUPABASE_REALTIME_IP_VERSION"]; + delete testEnv["SUPABASE_REALTIME_IP_VERSION"]; }); it("falls back to the configured value when unset", () => { - expect(envOverrideRealtimeIpVersion("IPv4", undefined)).toBe("IPv4"); + expect(run(envOverrideRealtimeIpVersion("IPv4", undefined))).toBe("IPv4"); }); it("overrides IPv4 to IPv6 via the env var", () => { - process.env["SUPABASE_REALTIME_IP_VERSION"] = "IPv6"; - expect(envOverrideRealtimeIpVersion("IPv4", undefined)).toBe("IPv6"); + testEnv["SUPABASE_REALTIME_IP_VERSION"] = "IPv6"; + expect(run(envOverrideRealtimeIpVersion("IPv4", undefined))).toBe("IPv6"); }); it("rejects an invalid override instead of falling back to the configured value", () => { - process.env["SUPABASE_REALTIME_IP_VERSION"] = "IPv5"; - expect(() => envOverrideRealtimeIpVersion("IPv4", undefined)).toThrow( + testEnv["SUPABASE_REALTIME_IP_VERSION"] = "IPv5"; + expect(() => run(envOverrideRealtimeIpVersion("IPv4", undefined))).toThrow( InvalidRealtimeIpVersionEnvOverrideError, ); - expect(() => envOverrideRealtimeIpVersion("IPv4", undefined)).toThrow( + expect(() => run(envOverrideRealtimeIpVersion("IPv4", undefined))).toThrow( 'Invalid config for realtime.ip_version: cannot parse "IPv5" as one of "IPv4", "IPv6"', ); }); @@ -933,41 +970,43 @@ describe("resolveLocalConfigValues", () => { describe("envOverrideRealtimeMaxHeaderLength", () => { afterEach(() => { - delete process.env["SUPABASE_REALTIME_MAX_HEADER_LENGTH"]; + delete testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"]; }); it("falls back to the configured value when unset", () => { - expect(envOverrideRealtimeMaxHeaderLength(4096, undefined)).toBe(4096); + expect(run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).toBe(4096); }); it("overrides the configured value via the env var", () => { - process.env["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "8192"; - expect(envOverrideRealtimeMaxHeaderLength(4096, undefined)).toBe(8192); + testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "8192"; + expect(run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).toBe(8192); }); it("also honors a projectEnvValues (dotenv) value", () => { expect( - envOverrideRealtimeMaxHeaderLength(4096, { - SUPABASE_REALTIME_MAX_HEADER_LENGTH: "16384", - }), + run( + envOverrideRealtimeMaxHeaderLength(4096, { + SUPABASE_REALTIME_MAX_HEADER_LENGTH: "16384", + }), + ), ).toBe(16384); }); it("rejects an override exceeding the uint64 max (2^64), matching Go's ParseUint failure", () => { - process.env["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "18446744073709551616"; - expect(() => envOverrideRealtimeMaxHeaderLength(4096, undefined)).toThrow( + testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "18446744073709551616"; + expect(() => run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).toThrow( "Failed reading config: Invalid realtime.max_header_length: 18446744073709551616.", ); }); it("accepts an override of exactly the uint64 max (2^64-1)", () => { - process.env["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "18446744073709551615"; - expect(() => envOverrideRealtimeMaxHeaderLength(4096, undefined)).not.toThrow(); + testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "18446744073709551615"; + expect(() => run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).not.toThrow(); }); it("rejects a hex override exceeding the uint64 max (2^64), matching Go's ParseUint failure", () => { - process.env["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "0x10000000000000000"; - expect(() => envOverrideRealtimeMaxHeaderLength(4096, undefined)).toThrow( + testEnv["SUPABASE_REALTIME_MAX_HEADER_LENGTH"] = "0x10000000000000000"; + expect(() => run(envOverrideRealtimeMaxHeaderLength(4096, undefined))).toThrow( "Failed reading config: Invalid realtime.max_header_length: 0x10000000000000000.", ); }); @@ -975,16 +1014,16 @@ describe("resolveLocalConfigValues", () => { describe("envOverrideApiMaxRows", () => { afterEach(() => { - delete process.env["SUPABASE_API_MAX_ROWS"]; + delete testEnv["SUPABASE_API_MAX_ROWS"]; }); it("falls back to the configured value when unset", () => { - expect(envOverrideApiMaxRows(1000, undefined)).toBe(1000); + expect(run(envOverrideApiMaxRows(1000, undefined))).toBe(1000); }); it("overrides the configured value via the env var", () => { - process.env["SUPABASE_API_MAX_ROWS"] = "500"; - expect(envOverrideApiMaxRows(1000, undefined)).toBe(500); + testEnv["SUPABASE_API_MAX_ROWS"] = "500"; + expect(run(envOverrideApiMaxRows(1000, undefined))).toBe(500); }); }); @@ -993,48 +1032,48 @@ describe("resolveLocalConfigValues", () => { // reject. describe("envOverrideMajorVersion", () => { afterEach(() => { - delete process.env["SUPABASE_DB_MAJOR_VERSION"]; + delete testEnv["SUPABASE_DB_MAJOR_VERSION"]; }); it("falls back to the configured value when unset", () => { - expect(envOverrideMajorVersion(17, undefined)).toBe(17); + expect(run(envOverrideMajorVersion(17, undefined))).toBe(17); }); it("resolves an octal leading-zero override to its octal value, not decimal", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "010"; - expect(envOverrideMajorVersion(17, undefined)).toBe(8); + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "010"; + expect(run(envOverrideMajorVersion(17, undefined))).toBe(8); }); it("resolves a 0x-prefixed override as hex", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "0x10"; - expect(envOverrideMajorVersion(17, undefined)).toBe(16); + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "0x10"; + expect(run(envOverrideMajorVersion(17, undefined))).toBe(16); }); it("resolves a 0b-prefixed override as binary", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "0b101"; - expect(envOverrideMajorVersion(17, undefined)).toBe(5); + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "0b101"; + expect(run(envOverrideMajorVersion(17, undefined))).toBe(5); }); it("still resolves a plain decimal override with no leading zero", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "15"; - expect(envOverrideMajorVersion(17, undefined)).toBe(15); + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "15"; + expect(run(envOverrideMajorVersion(17, undefined))).toBe(15); }); it("permits an underscore digit separator between decimal digits", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "1_000"; - expect(envOverrideMajorVersion(17, undefined)).toBe(1000); + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "1_000"; + expect(run(envOverrideMajorVersion(17, undefined))).toBe(1000); }); it("rejects an invalid octal digit instead of silently falling back to decimal", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "08"; - expect(() => envOverrideMajorVersion(17, undefined)).toThrow( + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "08"; + expect(() => run(envOverrideMajorVersion(17, undefined))).toThrow( "Failed reading config: Invalid db.major_version: 08.", ); }); it("rejects a signed override", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "+5"; - expect(() => envOverrideMajorVersion(17, undefined)).toThrow( + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "+5"; + expect(() => run(envOverrideMajorVersion(17, undefined))).toThrow( "Failed reading config: Invalid db.major_version: +5.", ); }); @@ -1042,24 +1081,24 @@ describe("resolveLocalConfigValues", () => { describe("envOverridePoolMode", () => { afterEach(() => { - delete process.env["SUPABASE_DB_POOLER_POOL_MODE"]; + delete testEnv["SUPABASE_DB_POOLER_POOL_MODE"]; }); it("falls back to the configured value when unset", () => { - expect(envOverridePoolMode("transaction", undefined)).toBe("transaction"); + expect(run(envOverridePoolMode("transaction", undefined))).toBe("transaction"); }); it("overrides the configured value via the env var", () => { - process.env["SUPABASE_DB_POOLER_POOL_MODE"] = "session"; - expect(envOverridePoolMode("transaction", undefined)).toBe("session"); + testEnv["SUPABASE_DB_POOLER_POOL_MODE"] = "session"; + expect(run(envOverridePoolMode("transaction", undefined))).toBe("session"); }); it("rejects an invalid override instead of falling back to the configured value", () => { - process.env["SUPABASE_DB_POOLER_POOL_MODE"] = "invalid"; - expect(() => envOverridePoolMode("transaction", undefined)).toThrow( + testEnv["SUPABASE_DB_POOLER_POOL_MODE"] = "invalid"; + expect(() => run(envOverridePoolMode("transaction", undefined))).toThrow( InvalidPoolModeEnvOverrideError, ); - expect(() => envOverridePoolMode("transaction", undefined)).toThrow( + expect(() => run(envOverridePoolMode("transaction", undefined))).toThrow( 'Invalid config for db.pooler.pool_mode: cannot parse "invalid" as one of "transaction", "session"', ); }); @@ -1067,24 +1106,24 @@ describe("resolveLocalConfigValues", () => { describe("envOverrideEdgeRuntimePolicy", () => { afterEach(() => { - delete process.env["SUPABASE_EDGE_RUNTIME_POLICY"]; + delete testEnv["SUPABASE_EDGE_RUNTIME_POLICY"]; }); it("falls back to the configured value when unset", () => { - expect(envOverrideEdgeRuntimePolicy("oneshot", undefined)).toBe("oneshot"); + expect(run(envOverrideEdgeRuntimePolicy("oneshot", undefined))).toBe("oneshot"); }); it("overrides the configured value via the env var", () => { - process.env["SUPABASE_EDGE_RUNTIME_POLICY"] = "per_worker"; - expect(envOverrideEdgeRuntimePolicy("oneshot", undefined)).toBe("per_worker"); + testEnv["SUPABASE_EDGE_RUNTIME_POLICY"] = "per_worker"; + expect(run(envOverrideEdgeRuntimePolicy("oneshot", undefined))).toBe("per_worker"); }); it("rejects an invalid override instead of falling back to the configured value", () => { - process.env["SUPABASE_EDGE_RUNTIME_POLICY"] = "invalid"; - expect(() => envOverrideEdgeRuntimePolicy("oneshot", undefined)).toThrow( + testEnv["SUPABASE_EDGE_RUNTIME_POLICY"] = "invalid"; + expect(() => run(envOverrideEdgeRuntimePolicy("oneshot", undefined))).toThrow( InvalidEdgeRuntimePolicyEnvOverrideError, ); - expect(() => envOverrideEdgeRuntimePolicy("oneshot", undefined)).toThrow( + expect(() => run(envOverrideEdgeRuntimePolicy("oneshot", undefined))).toThrow( 'Invalid config for edge_runtime.policy: cannot parse "invalid" as one of "per_worker", "oneshot"', ); }); @@ -1092,154 +1131,169 @@ describe("resolveLocalConfigValues", () => { describe("envOverrideDefaultPoolSize", () => { afterEach(() => { - delete process.env["SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE"]; + delete testEnv["SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE"]; }); it("falls back to the configured value when unset", () => { - expect(envOverrideDefaultPoolSize(20, undefined)).toBe(20); + expect(run(envOverrideDefaultPoolSize(20, undefined))).toBe(20); }); it("overrides the configured value via the env var", () => { - process.env["SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE"] = "40"; - expect(envOverrideDefaultPoolSize(20, undefined)).toBe(40); + testEnv["SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE"] = "40"; + expect(run(envOverrideDefaultPoolSize(20, undefined))).toBe(40); }); }); describe("envOverrideMaxClientConn", () => { afterEach(() => { - delete process.env["SUPABASE_DB_POOLER_MAX_CLIENT_CONN"]; + delete testEnv["SUPABASE_DB_POOLER_MAX_CLIENT_CONN"]; }); it("falls back to the configured value when unset", () => { - expect(envOverrideMaxClientConn(100, undefined)).toBe(100); + expect(run(envOverrideMaxClientConn(100, undefined))).toBe(100); }); it("overrides the configured value via the env var", () => { - process.env["SUPABASE_DB_POOLER_MAX_CLIENT_CONN"] = "200"; - expect(envOverrideMaxClientConn(100, undefined)).toBe(200); + testEnv["SUPABASE_DB_POOLER_MAX_CLIENT_CONN"] = "200"; + expect(run(envOverrideMaxClientConn(100, undefined))).toBe(200); }); }); describe("resolveAuthCaptcha", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"]; - delete process.env["SUPABASE_AUTH_CAPTCHA_PROVIDER"]; - delete process.env["SUPABASE_AUTH_CAPTCHA_SECRET"]; + delete testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"]; + delete testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"]; + delete testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"]; }); it("returns undefined when captcha is not configured", () => { - expect(resolveAuthCaptcha(undefined, undefined, undefined)).toBeUndefined(); + expect(run(resolveAuthCaptcha(undefined, undefined, undefined))).toBeUndefined(); }); it("overrides enabled/provider when the section is present in the document", () => { - process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; - process.env["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "turnstile"; + testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "turnstile"; const authDocument = { captcha: { enabled: false, provider: "hcaptcha" } }; - const resolved = resolveAuthCaptcha( - authDocument, - { enabled: false, provider: "hcaptcha", secret: "shh" }, - undefined, + const resolved = run( + resolveAuthCaptcha( + authDocument, + { enabled: false, provider: "hcaptcha", secret: "shh" }, + undefined, + ), ); expect(resolved?.enabled).toBe(true); expect(resolved?.provider).toBe("turnstile"); }); it("does not apply an env override when [auth.captcha] is absent from the document", () => { - process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; - const resolved = resolveAuthCaptcha( - {}, - { enabled: false, provider: "hcaptcha", secret: "shh" }, - undefined, + testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; + const resolved = run( + resolveAuthCaptcha({}, { enabled: false, provider: "hcaptcha", secret: "shh" }, undefined), ); expect(resolved?.enabled).toBe(false); }); it("decrypts an encrypted: captcha secret", () => { - process.env["DOTENV_PRIVATE_KEY"] = - "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb"; const authDocument = { captcha: { enabled: true } }; - const resolved = resolveAuthCaptcha( - authDocument, - { - enabled: true, - provider: "hcaptcha", - secret: - "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/", - }, - undefined, + const resolved = run( + withEnvVar( + "DOTENV_PRIVATE_KEY", + "7fd7210cef8f331ee8c55897996aaaafd853a2b20a4dc73d6d75759f65d2a7eb", + resolveAuthCaptcha( + authDocument, + { + enabled: true, + provider: "hcaptcha", + secret: + "encrypted:BKiXH15AyRzeohGyUrmB6cGjSklCrrBjdesQlX1VcXo/Xp20Bi2gGZ3AlIqxPQDmjVAALnhZamKnuY73l8Dz1P+BYiZUgxTSLzdCvdYUyVbNekj2UudbdUizBViERtZkuQwZHIv/", + }, + undefined, + ), + ), ); expect(resolved?.secret).toBe("value"); - delete process.env["DOTENV_PRIVATE_KEY"]; }); it("suppresses a malformed SUPABASE_AUTH_CAPTCHA_ENABLED when a remote block already set auth.captcha.enabled", () => { - process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "not-a-bool"; const authDocument = { captcha: { enabled: false } }; expect(() => - resolveAuthCaptcha( - authDocument, - { enabled: false, provider: "hcaptcha", secret: "shh" }, - undefined, - new Set(["auth.captcha.enabled"]), + run( + resolveAuthCaptcha( + authDocument, + { enabled: false, provider: "hcaptcha", secret: "shh" }, + undefined, + new Set(["auth.captcha.enabled"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_AUTH_CAPTCHA_ENABLED when no remote block matched", () => { - process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "not-a-bool"; const authDocument = { captcha: { enabled: false } }; expect(() => - resolveAuthCaptcha( - authDocument, - { enabled: false, provider: "hcaptcha", secret: "shh" }, - undefined, + run( + resolveAuthCaptcha( + authDocument, + { enabled: false, provider: "hcaptcha", secret: "shh" }, + undefined, + ), ), ).toThrow('cannot parse "not-a-bool" as a bool'); }); it("suppresses a malformed SUPABASE_AUTH_CAPTCHA_SECRET when a remote block already set auth.captcha.secret", () => { - process.env["SUPABASE_AUTH_CAPTCHA_SECRET"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"] = "encrypted:not-a-real-ciphertext"; const authDocument = { captcha: { enabled: true } }; - const resolved = resolveAuthCaptcha( - authDocument, - { enabled: true, provider: "hcaptcha", secret: "remote-secret" }, - undefined, - new Set(["auth.captcha.secret"]), + const resolved = run( + resolveAuthCaptcha( + authDocument, + { enabled: true, provider: "hcaptcha", secret: "remote-secret" }, + undefined, + new Set(["auth.captcha.secret"]), + ), ); expect(resolved?.secret).toBe("remote-secret"); }); it("still rejects a malformed SUPABASE_AUTH_CAPTCHA_SECRET when no remote block matched", () => { - process.env["SUPABASE_AUTH_CAPTCHA_SECRET"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"] = "encrypted:not-a-real-ciphertext"; const authDocument = { captcha: { enabled: true } }; expect(() => - resolveAuthCaptcha( - authDocument, - { enabled: true, provider: "hcaptcha", secret: "remote-secret" }, - undefined, + run( + resolveAuthCaptcha( + authDocument, + { enabled: true, provider: "hcaptcha", secret: "remote-secret" }, + undefined, + ), ), ).toThrow("failed to parse config: missing private key"); }); it("preserves a remote block's valid auth.captcha.provider over an unsupported ambient override", () => { - process.env["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "recaptcha"; + testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "recaptcha"; const authDocument = { captcha: { enabled: true, provider: "hcaptcha" } }; - const resolved = resolveAuthCaptcha( - authDocument, - { enabled: true, provider: "hcaptcha", secret: "shh" }, - undefined, - new Set(["auth.captcha.provider"]), + const resolved = run( + resolveAuthCaptcha( + authDocument, + { enabled: true, provider: "hcaptcha", secret: "shh" }, + undefined, + new Set(["auth.captcha.provider"]), + ), ); expect(resolved?.provider).toBe("hcaptcha"); }); it("still applies SUPABASE_AUTH_CAPTCHA_PROVIDER when no remote block matched", () => { - process.env["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "turnstile"; + testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "turnstile"; const authDocument = { captcha: { enabled: true, provider: "hcaptcha" } }; - const resolved = resolveAuthCaptcha( - authDocument, - { enabled: true, provider: "hcaptcha", secret: "shh" }, - undefined, + const resolved = run( + resolveAuthCaptcha( + authDocument, + { enabled: true, provider: "hcaptcha", secret: "shh" }, + undefined, + ), ); expect(resolved?.provider).toBe("turnstile"); }); @@ -1247,7 +1301,7 @@ describe("resolveLocalConfigValues", () => { describe("resolveAuthEmail", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_CONFIRMATION_SUBJECT"]; + delete testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_CONFIRMATION_SUBJECT"]; }); it("keeps an explicit empty subject present in the raw document, not omitted", () => { @@ -1255,7 +1309,7 @@ describe("resolveLocalConfigValues", () => { auth: { email: { template: { confirmation: { subject: "", content_path: "x" } } } }, }); const authDocument = { email: { template: { confirmation: { subject: "" } } } }; - const resolved = resolveAuthEmail(config.auth.email, authDocument, undefined); + const resolved = run(resolveAuthEmail(config.auth.email, authDocument, undefined)); expect(resolved.template["confirmation"]?.subject).toBe(""); }); @@ -1264,41 +1318,43 @@ describe("resolveLocalConfigValues", () => { auth: { email: { template: { confirmation: { content_path: "x" } } } }, }); const authDocument = { email: { template: { confirmation: { content_path: "x" } } } }; - const resolved = resolveAuthEmail(config.auth.email, authDocument, undefined); + const resolved = run(resolveAuthEmail(config.auth.email, authDocument, undefined)); expect(resolved.template["confirmation"]?.subject).toBeUndefined(); }); it("prefers an env-overridden subject over the raw document's presence, even when absent", () => { - process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_CONFIRMATION_SUBJECT"] = "Overridden subject"; + testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_CONFIRMATION_SUBJECT"] = "Overridden subject"; const config = baseConfig({ auth: { email: { template: { confirmation: { content_path: "x" } } } }, }); const authDocument = { email: { template: { confirmation: { content_path: "x" } } } }; - const resolved = resolveAuthEmail(config.auth.email, authDocument, undefined); + const resolved = run(resolveAuthEmail(config.auth.email, authDocument, undefined)); expect(resolved.template["confirmation"]?.subject).toBe("Overridden subject"); }); describe("max_frequency — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"]; + delete testEnv["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"]; }); it("prefers a remote-set auth.email.max_frequency over a conflicting SUPABASE_AUTH_EMAIL_MAX_FREQUENCY", () => { - process.env["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"] = "5s"; + testEnv["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"] = "5s"; const config = baseConfig({ auth: { email: { max_frequency: "1m" } } }); - const resolved = resolveAuthEmail( - config.auth.email, - undefined, - undefined, - new Set(["auth.email.max_frequency"]), + const resolved = run( + resolveAuthEmail( + config.auth.email, + undefined, + undefined, + new Set(["auth.email.max_frequency"]), + ), ); expect(resolved.max_frequency).toBe("1m"); }); it("still applies SUPABASE_AUTH_EMAIL_MAX_FREQUENCY when no remote block matched", () => { - process.env["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"] = "5s"; + testEnv["SUPABASE_AUTH_EMAIL_MAX_FREQUENCY"] = "5s"; const config = baseConfig({ auth: { email: { max_frequency: "1m" } } }); - const resolved = resolveAuthEmail(config.auth.email, undefined, undefined); + const resolved = run(resolveAuthEmail(config.auth.email, undefined, undefined)); expect(resolved.max_frequency).toBe("5s"); }); }); @@ -1316,285 +1372,285 @@ describe("resolveLocalConfigValues", () => { }; afterEach(() => { - delete process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"]; - delete process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"]; - delete process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS"]; + delete testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"]; + delete testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"]; + delete testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS"]; }); it("leaves every hook disabled when nothing is configured or overridden", () => { - const resolved = resolveAuthHooks(undefined, allHooks, undefined); + const resolved = run(resolveAuthHooks(undefined, allHooks, undefined)); expect(resolved.customAccessToken.enabled).toBe(false); expect(resolved.mfaVerificationAttempt.enabled).toBe(false); }); it("overrides enabled/uri when the hook's section is present in the document", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "true"; - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "https://example.com/hook"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "https://example.com/hook"; const authDocument = { hook: { custom_access_token: { enabled: false } } }; - const resolved = resolveAuthHooks(authDocument, allHooks, undefined); + const resolved = run(resolveAuthHooks(authDocument, allHooks, undefined)); expect(resolved.customAccessToken.enabled).toBe(true); expect(resolved.customAccessToken.uri).toBe("https://example.com/hook"); expect(resolved.mfaVerificationAttempt.enabled).toBe(false); }); it("does not apply an env override when the hook's section is absent from the document", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "true"; - const resolved = resolveAuthHooks({}, allHooks, undefined); + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "true"; + const resolved = run(resolveAuthHooks({}, allHooks, undefined)); expect(resolved.customAccessToken.enabled).toBe(false); }); it("suppresses a malformed SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED when a remote block already set that hook's enabled", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "not-a-bool"; const authDocument = { hook: { custom_access_token: { enabled: false } } }; expect(() => - resolveAuthHooks( - authDocument, - allHooks, - undefined, - new Set(["auth.hook.custom_access_token.enabled"]), + run( + resolveAuthHooks( + authDocument, + allHooks, + undefined, + new Set(["auth.hook.custom_access_token.enabled"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED when no remote block matched", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED"] = "not-a-bool"; const authDocument = { hook: { custom_access_token: { enabled: false } } }; - expect(() => resolveAuthHooks(authDocument, allHooks, undefined)).toThrow( + expect(() => run(resolveAuthHooks(authDocument, allHooks, undefined))).toThrow( 'cannot parse "not-a-bool" as a bool', ); }); it("prefers a remote-set auth.hook.custom_access_token.uri over a conflicting SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; const hooksWithRemoteUri = { ...allHooks, custom_access_token: { enabled: true, uri: "https://example.com/hook", secrets: "" }, }; const authDocument = { hook: { custom_access_token: { enabled: true } } }; - const resolved = resolveAuthHooks( - authDocument, - hooksWithRemoteUri, - undefined, - new Set(["auth.hook.custom_access_token.uri"]), + const resolved = run( + resolveAuthHooks( + authDocument, + hooksWithRemoteUri, + undefined, + new Set(["auth.hook.custom_access_token.uri"]), + ), ); expect(resolved.customAccessToken.uri).toBe("https://example.com/hook"); }); it("prefers a remote-set auth.hook.custom_access_token.secrets over a conflicting SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS"] = "env-secret"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS"] = "env-secret"; const hooksWithRemoteSecrets = { ...allHooks, custom_access_token: { enabled: true, uri: "", secrets: "remote-secret" }, }; const authDocument = { hook: { custom_access_token: { enabled: true } } }; - const resolved = resolveAuthHooks( - authDocument, - hooksWithRemoteSecrets, - undefined, - new Set(["auth.hook.custom_access_token.secrets"]), + const resolved = run( + resolveAuthHooks( + authDocument, + hooksWithRemoteSecrets, + undefined, + new Set(["auth.hook.custom_access_token.secrets"]), + ), ); expect(resolved.customAccessToken.secrets).toBe("remote-secret"); }); it("still applies the env override for uri when no remote block matched that leaf", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "https://env.example.com/hook"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "https://env.example.com/hook"; const hooksWithLocalUri = { ...allHooks, custom_access_token: { enabled: true, uri: "https://local.example.com/hook", secrets: "" }, }; const authDocument = { hook: { custom_access_token: { enabled: true } } }; - const resolved = resolveAuthHooks(authDocument, hooksWithLocalUri, undefined); + const resolved = run(resolveAuthHooks(authDocument, hooksWithLocalUri, undefined)); expect(resolved.customAccessToken.uri).toBe("https://env.example.com/hook"); }); }); describe("resolveAuthMfa — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"]; + delete testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"]; }); it("suppresses a malformed SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED when a remote block already set auth.mfa.totp.enroll_enabled", () => { - process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; const mfa = baseConfig().auth.mfa; expect(() => - resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.totp.enroll_enabled"])), + run(resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.totp.enroll_enabled"]))), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED when no remote block matched", () => { - process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; const mfa = baseConfig().auth.mfa; - expect(() => resolveAuthMfa(mfa, undefined)).toThrow('cannot parse "not-a-bool" as a bool'); + expect(() => run(resolveAuthMfa(mfa, undefined))).toThrow( + 'cannot parse "not-a-bool" as a bool', + ); }); it("prefers a remote-set auth.mfa.phone.template over a conflicting SUPABASE_AUTH_MFA_PHONE_TEMPLATE", () => { - process.env["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"] = "env template"; + testEnv["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"] = "env template"; const mfa = { ...baseConfig().auth.mfa, phone: { ...baseConfig().auth.mfa.phone, template: "remote template" }, }; - const resolved = resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.phone.template"])); + const resolved = run(resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.phone.template"]))); expect(resolved.phone.template).toBe("remote template"); - delete process.env["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"]; + delete testEnv["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"]; }); it("still applies SUPABASE_AUTH_MFA_PHONE_TEMPLATE when no remote block matched", () => { - process.env["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"] = "env template"; + testEnv["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"] = "env template"; const mfa = { ...baseConfig().auth.mfa, phone: { ...baseConfig().auth.mfa.phone, template: "remote template" }, }; - const resolved = resolveAuthMfa(mfa, undefined); + const resolved = run(resolveAuthMfa(mfa, undefined)); expect(resolved.phone.template).toBe("env template"); - delete process.env["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"]; + delete testEnv["SUPABASE_AUTH_MFA_PHONE_TEMPLATE"]; }); it("prefers a remote-set auth.mfa.phone.max_frequency over a conflicting SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY", () => { - process.env["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"] = "5s"; + testEnv["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"] = "5s"; const mfa = { ...baseConfig().auth.mfa, phone: { ...baseConfig().auth.mfa.phone, max_frequency: "1m" }, }; - const resolved = resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.phone.max_frequency"])); + const resolved = run( + resolveAuthMfa(mfa, undefined, new Set(["auth.mfa.phone.max_frequency"])), + ); expect(resolved.phone.max_frequency).toBe("1m"); - delete process.env["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"]; + delete testEnv["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"]; }); it("still applies SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY when no remote block matched", () => { - process.env["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"] = "5s"; + testEnv["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"] = "5s"; const mfa = { ...baseConfig().auth.mfa, phone: { ...baseConfig().auth.mfa.phone, max_frequency: "1m" }, }; - const resolved = resolveAuthMfa(mfa, undefined); + const resolved = run(resolveAuthMfa(mfa, undefined)); expect(resolved.phone.max_frequency).toBe("5s"); - delete process.env["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"]; + delete testEnv["SUPABASE_AUTH_MFA_PHONE_MAX_FREQUENCY"]; }); }); describe("resolveAuthEmailSmtp — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"]; - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"]; }); it("suppresses a malformed SUPABASE_AUTH_EMAIL_SMTP_ENABLED when a remote block already set auth.email.smtp.enabled", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "not-a-bool"; const authDocument = { email: { smtp: { enabled: true } } }; expect(() => - resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.enabled"])), + run(resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.enabled"]))), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_AUTH_EMAIL_SMTP_ENABLED when no remote block matched", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "not-a-bool"; const authDocument = { email: { smtp: { enabled: true } } }; - expect(() => resolveAuthEmailSmtp(authDocument, undefined)).toThrow( + expect(() => run(resolveAuthEmailSmtp(authDocument, undefined))).toThrow( 'cannot parse "not-a-bool" as a bool', ); }); it("suppresses a malformed SUPABASE_AUTH_EMAIL_SMTP_PASS when a remote block already set auth.email.smtp.pass", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "encrypted:not-a-real-ciphertext"; const authDocument = { email: { smtp: { enabled: true, pass: "remote-pass" } } }; - const resolved = resolveAuthEmailSmtp( - authDocument, - undefined, - new Set(["auth.email.smtp.pass"]), + const resolved = run( + resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.pass"])), ); expect(resolved?.pass).toBe("remote-pass"); }); it("still rejects a malformed SUPABASE_AUTH_EMAIL_SMTP_PASS when no remote block matched", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "encrypted:not-a-real-ciphertext"; const authDocument = { email: { smtp: { enabled: true, pass: "remote-pass" } } }; - expect(() => resolveAuthEmailSmtp(authDocument, undefined)).toThrow( + expect(() => run(resolveAuthEmailSmtp(authDocument, undefined))).toThrow( "failed to parse config: missing private key", ); }); it("prefers a remote-set auth.email.smtp.host over a conflicting SUPABASE_AUTH_EMAIL_SMTP_HOST", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.env.example.com"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.env.example.com"; const authDocument = { email: { smtp: { enabled: true, host: "smtp.remote.example.com" } } }; - const resolved = resolveAuthEmailSmtp( - authDocument, - undefined, - new Set(["auth.email.smtp.host"]), + const resolved = run( + resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.host"])), ); expect(resolved?.host).toBe("smtp.remote.example.com"); - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; }); it("still applies SUPABASE_AUTH_EMAIL_SMTP_HOST when no remote block matched", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.env.example.com"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.env.example.com"; const authDocument = { email: { smtp: { enabled: true, host: "smtp.remote.example.com" } } }; - const resolved = resolveAuthEmailSmtp(authDocument, undefined); + const resolved = run(resolveAuthEmailSmtp(authDocument, undefined)); expect(resolved?.host).toBe("smtp.env.example.com"); - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; }); it("prefers a remote-set auth.email.smtp.user over a conflicting SUPABASE_AUTH_EMAIL_SMTP_USER", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "env-user"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "env-user"; const authDocument = { email: { smtp: { enabled: true, user: "remote-user" } } }; - const resolved = resolveAuthEmailSmtp( - authDocument, - undefined, - new Set(["auth.email.smtp.user"]), + const resolved = run( + resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.user"])), ); expect(resolved?.user).toBe("remote-user"); - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"]; }); it("still applies SUPABASE_AUTH_EMAIL_SMTP_USER when no remote block matched", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "env-user"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "env-user"; const authDocument = { email: { smtp: { enabled: true, user: "remote-user" } } }; - const resolved = resolveAuthEmailSmtp(authDocument, undefined); + const resolved = run(resolveAuthEmailSmtp(authDocument, undefined)); expect(resolved?.user).toBe("env-user"); - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"]; }); it("prefers a remote-set auth.email.smtp.admin_email over a conflicting SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "env@example.com"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "env@example.com"; const authDocument = { email: { smtp: { enabled: true, admin_email: "remote@example.com" } }, }; - const resolved = resolveAuthEmailSmtp( - authDocument, - undefined, - new Set(["auth.email.smtp.admin_email"]), + const resolved = run( + resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.admin_email"])), ); expect(resolved?.adminEmail).toBe("remote@example.com"); - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; }); it("still applies SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL when no remote block matched", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "env@example.com"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "env@example.com"; const authDocument = { email: { smtp: { enabled: true, admin_email: "remote@example.com" } }, }; - const resolved = resolveAuthEmailSmtp(authDocument, undefined); + const resolved = run(resolveAuthEmailSmtp(authDocument, undefined)); expect(resolved?.adminEmail).toBe("env@example.com"); - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; }); it("prefers a remote-set auth.email.smtp.sender_name over a conflicting SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"] = "Env Sender"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"] = "Env Sender"; const authDocument = { email: { smtp: { enabled: true, sender_name: "Remote Sender" } } }; - const resolved = resolveAuthEmailSmtp( - authDocument, - undefined, - new Set(["auth.email.smtp.sender_name"]), + const resolved = run( + resolveAuthEmailSmtp(authDocument, undefined, new Set(["auth.email.smtp.sender_name"])), ); expect(resolved?.senderName).toBe("Remote Sender"); - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"]; }); it("still applies SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME when no remote block matched", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"] = "Env Sender"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"] = "Env Sender"; const authDocument = { email: { smtp: { enabled: true, sender_name: "Remote Sender" } } }; - const resolved = resolveAuthEmailSmtp(authDocument, undefined); + const resolved = run(resolveAuthEmailSmtp(authDocument, undefined)); expect(resolved?.senderName).toBe("Env Sender"); - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_SENDER_NAME"]; }); }); @@ -1610,10 +1666,8 @@ describe("resolveLocalConfigValues", () => { }, }, }; - const resolved = resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - undefined, + const resolved = run( + resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined), ); expect(resolved["my_custom"]?.enabled).toBe(true); expect(resolved["my_custom"]?.skipNonceCheck).toBe(false); @@ -1625,7 +1679,7 @@ describe("resolveLocalConfigValues", () => { external: { my_custom: { enabled: "not-a-bool", client_id: "custom-client-id" } }, }; expect(() => - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined), + run(resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined)), ).toThrow('cannot parse "not-a-bool" as a bool'); }); @@ -1633,10 +1687,8 @@ describe("resolveLocalConfigValues", () => { const authDocument = { external: { my_custom: { client_id: "custom-client-id" } }, }; - const resolved = resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - undefined, + const resolved = run( + resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined), ); expect(resolved["my_custom"]?.enabled).toBe(false); }); @@ -1645,10 +1697,8 @@ describe("resolveLocalConfigValues", () => { const authDocument = { external: { my_custom: { enabled: 1, client_id: "custom-client-id" } }, }; - const resolved = resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - undefined, + const resolved = run( + resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined), ); expect(resolved["my_custom"]?.enabled).toBe(true); }); @@ -1658,7 +1708,7 @@ describe("resolveLocalConfigValues", () => { external: { my_custom: { enabled: [1, 2], client_id: "custom-client-id" } }, }; expect(() => - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined), + run(resolveAuthExternalProviders(authDocument, baseConfig().auth.external, undefined)), ).toThrow('cannot parse "1,2" as a bool'); }); @@ -1669,10 +1719,8 @@ describe("resolveLocalConfigValues", () => { SUPABASE_AUTH_EXTERNAL_APPLE_SECRET: "apple-secret", SUPABASE_AUTH_EXTERNAL_APPLE_URL: "https://appleid.apple.com", }; - const resolved = resolveAuthExternalProviders( - undefined, - baseConfig().auth.external, - projectEnvValues, + const resolved = run( + resolveAuthExternalProviders(undefined, baseConfig().auth.external, projectEnvValues), ); expect(resolved["apple"]).toEqual({ enabled: true, @@ -1690,10 +1738,8 @@ describe("resolveLocalConfigValues", () => { SUPABASE_AUTH_EXTERNAL_GOOGLE_ENABLED: "true", SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID: "google-client-id", }; - const resolved = resolveAuthExternalProviders( - undefined, - baseConfig().auth.external, - projectEnvValues, + const resolved = run( + resolveAuthExternalProviders(undefined, baseConfig().auth.external, projectEnvValues), ); expect(resolved["google"]).toBeUndefined(); expect(resolved["apple"]?.enabled).toBe(false); @@ -1706,11 +1752,13 @@ describe("resolveLocalConfigValues", () => { external: { my_custom: { enabled: true, secret: "remote-secret" } }, }; const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_SECRET: "encrypted:garbage" }; - const resolved = resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - projectEnvValues, - new Set(["auth.external.my_custom.secret"]), + const resolved = run( + resolveAuthExternalProviders( + authDocument, + baseConfig().auth.external, + projectEnvValues, + new Set(["auth.external.my_custom.secret"]), + ), ); expect(resolved["my_custom"]?.secret).toBe("remote-secret"); }); @@ -1721,7 +1769,9 @@ describe("resolveLocalConfigValues", () => { }; const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_SECRET: "encrypted:garbage" }; expect(() => - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, projectEnvValues), + run( + resolveAuthExternalProviders(authDocument, baseConfig().auth.external, projectEnvValues), + ), ).toThrow("failed to parse config: missing private key"); }); @@ -1729,11 +1779,13 @@ describe("resolveLocalConfigValues", () => { const authDocument = { external: { my_custom: { enabled: true } } }; const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_ENABLED: "not-a-bool" }; expect(() => - resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - projectEnvValues, - new Set(["auth.external.my_custom.enabled"]), + run( + resolveAuthExternalProviders( + authDocument, + baseConfig().auth.external, + projectEnvValues, + new Set(["auth.external.my_custom.enabled"]), + ), ), ).not.toThrow(); }); @@ -1742,7 +1794,9 @@ describe("resolveLocalConfigValues", () => { const authDocument = { external: { my_custom: { enabled: true } } }; const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_ENABLED: "not-a-bool" }; expect(() => - resolveAuthExternalProviders(authDocument, baseConfig().auth.external, projectEnvValues), + run( + resolveAuthExternalProviders(authDocument, baseConfig().auth.external, projectEnvValues), + ), ).toThrow('cannot parse "not-a-bool" as a bool'); }); @@ -1753,11 +1807,13 @@ describe("resolveLocalConfigValues", () => { const projectEnvValues = { SUPABASE_AUTH_EXTERNAL_MY_CUSTOM_CLIENT_ID: "env-should-not-win", }; - const resolved = resolveAuthExternalProviders( - authDocument, - baseConfig().auth.external, - projectEnvValues, - new Set(["auth.external.my_custom.client_id"]), + const resolved = run( + resolveAuthExternalProviders( + authDocument, + baseConfig().auth.external, + projectEnvValues, + new Set(["auth.external.my_custom.client_id"]), + ), ); expect(resolved["my_custom"]?.clientId).toBe("remote-client-id"); }); @@ -1765,37 +1821,37 @@ describe("resolveLocalConfigValues", () => { describe("rawUnmodeledBool", () => { it("returns false for an absent value, matching Go's zero-value bool default", () => { - expect(rawUnmodeledBool(undefined, "auth.passkey.enabled")).toBe(false); + expect(run(rawUnmodeledBool(undefined, "auth.passkey.enabled"))).toBe(false); }); it("passes a real boolean through unchanged", () => { - expect(rawUnmodeledBool(true, "auth.passkey.enabled")).toBe(true); - expect(rawUnmodeledBool(false, "auth.passkey.enabled")).toBe(false); + expect(run(rawUnmodeledBool(true, "auth.passkey.enabled"))).toBe(true); + expect(run(rawUnmodeledBool(false, "auth.passkey.enabled"))).toBe(false); }); it("weakly coerces a raw number by truthiness, matching mapstructure's WeaklyTypedInput decodeBool", () => { - expect(rawUnmodeledBool(123, "auth.passkey.enabled")).toBe(true); - expect(rawUnmodeledBool(0, "auth.passkey.enabled")).toBe(false); - expect(rawUnmodeledBool(1.5, "auth.passkey.enabled")).toBe(true); + expect(run(rawUnmodeledBool(123, "auth.passkey.enabled"))).toBe(true); + expect(run(rawUnmodeledBool(0, "auth.passkey.enabled"))).toBe(false); + expect(run(rawUnmodeledBool(1.5, "auth.passkey.enabled"))).toBe(true); }); it("parses a valid boolean-ish string the way Go's strconv.ParseBool does", () => { - expect(rawUnmodeledBool("true", "auth.passkey.enabled")).toBe(true); - expect(rawUnmodeledBool("False", "auth.passkey.enabled")).toBe(false); - expect(rawUnmodeledBool("", "auth.passkey.enabled")).toBe(false); + expect(run(rawUnmodeledBool("true", "auth.passkey.enabled"))).toBe(true); + expect(run(rawUnmodeledBool("False", "auth.passkey.enabled"))).toBe(false); + expect(run(rawUnmodeledBool("", "auth.passkey.enabled"))).toBe(false); }); it("throws on an unparsable string instead of silently disabling it", () => { - expect(() => rawUnmodeledBool("not-a-bool", "auth.passkey.enabled")).toThrow( + expect(() => run(rawUnmodeledBool("not-a-bool", "auth.passkey.enabled"))).toThrow( 'cannot parse "not-a-bool" as a bool', ); }); it("throws on an array or table value — mapstructure's decodeBool errors on these unconditionally, never weakly coerced", () => { - expect(() => rawUnmodeledBool([1, 2], "auth.passkey.enabled")).toThrow( + expect(() => run(rawUnmodeledBool([1, 2], "auth.passkey.enabled"))).toThrow( InvalidBoolEnvOverrideError, ); - expect(() => rawUnmodeledBool({ nested: true }, "auth.passkey.enabled")).toThrow( + expect(() => run(rawUnmodeledBool({ nested: true }, "auth.passkey.enabled"))).toThrow( InvalidBoolEnvOverrideError, ); }); @@ -1829,98 +1885,102 @@ describe("resolveLocalConfigValues", () => { ]; afterEach(() => { - for (const name of ALL_OVERRIDE_NAMES) delete process.env[name]; + for (const name of ALL_OVERRIDE_NAMES) delete testEnv[name]; }); it("returns the configured settings unchanged when nothing is overridden", () => { const settings = { shared_buffers: "128MB", max_connections: 100 }; - expect(resolveDbSettingsEnvOverrides(settings, undefined)).toEqual(settings); + expect(run(resolveDbSettingsEnvOverrides(settings, undefined))).toEqual(settings); }); it("leaves an unconfigured field undefined when nothing is overridden", () => { - expect(resolveDbSettingsEnvOverrides({}, undefined).effective_cache_size).toBeUndefined(); + expect( + run(resolveDbSettingsEnvOverrides({}, undefined)).effective_cache_size, + ).toBeUndefined(); }); it("overrides a string field via the env var", () => { - process.env["SUPABASE_DB_SETTINGS_SHARED_BUFFERS"] = "256MB"; + testEnv["SUPABASE_DB_SETTINGS_SHARED_BUFFERS"] = "256MB"; expect( - resolveDbSettingsEnvOverrides({ shared_buffers: "128MB" }, undefined).shared_buffers, + run(resolveDbSettingsEnvOverrides({ shared_buffers: "128MB" }, undefined)).shared_buffers, ).toBe("256MB"); }); it("sets a string field via the env var even when not configured at all", () => { - process.env["SUPABASE_DB_SETTINGS_WORK_MEM"] = "8MB"; - expect(resolveDbSettingsEnvOverrides({}, undefined).work_mem).toBe("8MB"); + testEnv["SUPABASE_DB_SETTINGS_WORK_MEM"] = "8MB"; + expect(run(resolveDbSettingsEnvOverrides({}, undefined)).work_mem).toBe("8MB"); }); it("overrides a uint field via the env var", () => { - process.env["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "200"; + testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "200"; expect( - resolveDbSettingsEnvOverrides({ max_connections: 100 }, undefined).max_connections, + run(resolveDbSettingsEnvOverrides({ max_connections: 100 }, undefined)).max_connections, ).toBe(200); }); it("rejects a non-numeric uint override", () => { - process.env["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "not-a-number"; - expect(() => resolveDbSettingsEnvOverrides({}, undefined)).toThrow( + testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "not-a-number"; + expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( "Invalid db.settings.max_connections", ); }); it("resolves a 0x-prefixed uint override as hex", () => { - process.env["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "0x10"; + testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "0x10"; expect( - resolveDbSettingsEnvOverrides({ max_connections: 100 }, undefined).max_connections, + run(resolveDbSettingsEnvOverrides({ max_connections: 100 }, undefined)).max_connections, ).toBe(16); }); it("rejects a uint override exceeding the uint64 max (2^64), matching Go's ParseUint failure", () => { - process.env["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "18446744073709551616"; - expect(() => resolveDbSettingsEnvOverrides({}, undefined)).toThrow( + testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "18446744073709551616"; + expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( "Failed reading config: Invalid db.settings.max_connections: 18446744073709551616.", ); }); it("overrides the boolean field via the env var", () => { - process.env["SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP"] = "true"; + testEnv["SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP"] = "true"; expect( - resolveDbSettingsEnvOverrides({ track_commit_timestamp: false }, undefined) + run(resolveDbSettingsEnvOverrides({ track_commit_timestamp: false }, undefined)) .track_commit_timestamp, ).toBe(true); }); it("rejects a malformed boolean override", () => { - process.env["SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP"] = "not-a-bool"; - expect(() => resolveDbSettingsEnvOverrides({}, undefined)).toThrow( + testEnv["SUPABASE_DB_SETTINGS_TRACK_COMMIT_TIMESTAMP"] = "not-a-bool"; + expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( InvalidBoolEnvOverrideError, ); }); it("overrides the session_replication_role enum field via the env var", () => { - process.env["SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE"] = "replica"; + testEnv["SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE"] = "replica"; expect( - resolveDbSettingsEnvOverrides({ session_replication_role: "origin" }, undefined) + run(resolveDbSettingsEnvOverrides({ session_replication_role: "origin" }, undefined)) .session_replication_role, ).toBe("replica"); }); it("leaves session_replication_role undefined when neither configured nor overridden", () => { - expect(resolveDbSettingsEnvOverrides({}, undefined).session_replication_role).toBeUndefined(); + expect( + run(resolveDbSettingsEnvOverrides({}, undefined)).session_replication_role, + ).toBeUndefined(); }); it("rejects an invalid session_replication_role override", () => { - process.env["SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE"] = "invalid"; - expect(() => resolveDbSettingsEnvOverrides({}, undefined)).toThrow( + testEnv["SUPABASE_DB_SETTINGS_SESSION_REPLICATION_ROLE"] = "invalid"; + expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( InvalidSessionReplicationRoleEnvOverrideError, ); - expect(() => resolveDbSettingsEnvOverrides({}, undefined)).toThrow( + expect(() => run(resolveDbSettingsEnvOverrides({}, undefined))).toThrow( 'Invalid config for db.settings.session_replication_role: cannot parse "invalid" as one of "origin", "replica", "local"', ); }); it("also honors a projectEnvValues (dotenv) value", () => { expect( - resolveDbSettingsEnvOverrides({}, { SUPABASE_DB_SETTINGS_SHARED_BUFFERS: "512MB" }) + run(resolveDbSettingsEnvOverrides({}, { SUPABASE_DB_SETTINGS_SHARED_BUFFERS: "512MB" })) .shared_buffers, ).toBe("512MB"); }); @@ -1929,146 +1989,211 @@ describe("resolveLocalConfigValues", () => { describe("auth.signing_keys_path (asymmetric JWT signing)", () => { const tempRoot = useTempWorkdir("supabase-signing-keys-test-"); - it("signs anon/service_role with the first RS256 key in the file", async () => { - const jwk = generateRsaJwk(); - writeSigningKeys(tempRoot.current, [jwk]); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + it.live("signs anon/service_role with the first RS256 key in the file", () => + Effect.gen(function* () { + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + + const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; + const publicKey = yield* Effect.promise(() => importJWK(publicJwk, "RS256")); + const { payload, protectedHeader } = yield* Effect.promise(() => + jwtVerify(values.anonKey, publicKey), + ); + expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); + expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); - const publicJwk = { ...jwk, d: undefined, p: undefined, q: undefined, dp: undefined }; - const publicKey = await importJWK(publicJwk, "RS256"); - const { payload, protectedHeader } = await jwtVerify(values.anonKey, publicKey); - expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); - expect(protectedHeader).toMatchObject({ alg: "RS256", kid: "test-rsa-kid" }); + const serviceRole = yield* Effect.promise(() => + jwtVerify(values.serviceRoleKey, publicKey), + ); + expect(serviceRole.payload).toMatchObject({ role: "service_role" }); + }).pipe(Effect.provide(testLayer)), + ); - const serviceRole = await jwtVerify(values.serviceRoleKey, publicKey); - expect(serviceRole.payload).toMatchObject({ role: "service_role" }); - }); + it.live("resolves a relative signing_keys_path against /supabase", () => + Effect.gen(function* () { + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ auth: { signing_keys_path: "./signing_keys.json" } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + expect(values.anonKey.split(".")).toHaveLength(3); + }).pipe(Effect.provide(testLayer)), + ); - it("resolves a relative signing_keys_path against /supabase", async () => { - const jwk = generateRsaJwk(); - writeSigningKeys(tempRoot.current, [jwk]); - const config = baseConfig({ auth: { signing_keys_path: "./signing_keys.json" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - expect(values.anonKey.split(".")).toHaveLength(3); - }); + it.live("uses an absolute signing_keys_path as-is, without joining the workdir", () => + Effect.gen(function* () { + const path = yield* Path.Path; + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const absolutePath = path.join(tempRoot.current, "supabase", "signing_keys.json"); + const config = baseConfig({ auth: { signing_keys_path: absolutePath } }); + const values = yield* resolveLocalConfigValues( + config, + "127.0.0.1", + "/some/unrelated/workdir", + ); + expect(values.anonKey.split(".")).toHaveLength(3); + }).pipe(Effect.provide(testLayer)), + ); - it("uses an absolute signing_keys_path as-is, without joining the workdir", async () => { - const jwk = generateRsaJwk(); - writeSigningKeys(tempRoot.current, [jwk]); - const absolutePath = join(tempRoot.current, "supabase", "signing_keys.json"); - const config = baseConfig({ auth: { signing_keys_path: absolutePath } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", "/some/unrelated/workdir"); - expect(values.anonKey.split(".")).toHaveLength(3); - }); + it.live("still prefers an explicit anon_key/service_role_key over signing keys", () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); + const config = baseConfig({ + auth: { + signing_keys_path: "signing_keys.json", + anon_key: "configured-anon", + service_role_key: "configured-service-role", + }, + }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + expect(values.anonKey).toBe("configured-anon"); + expect(values.serviceRoleKey).toBe("configured-service-role"); + }).pipe(Effect.provide(testLayer)), + ); - it("still prefers an explicit anon_key/service_role_key over signing keys", () => { - writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - const config = baseConfig({ - auth: { - signing_keys_path: "signing_keys.json", - anon_key: "configured-anon", - service_role_key: "configured-service-role", - }, - }); - const values = resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - expect(values.anonKey).toBe("configured-anon"); - expect(values.serviceRoleKey).toBe("configured-service-role"); - }); + it.live("falls back to HMAC signing when signing_keys_path resolves to an empty array", () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, []); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + const [, payload] = values.anonKey.split("."); + expect(yield* decodeJson(Buffer.from(payload ?? "", "base64url").toString())).toMatchObject( + { + iss: "supabase-demo", + }, + ); + }).pipe(Effect.provide(testLayer)), + ); - it("falls back to HMAC signing when signing_keys_path resolves to an empty array", () => { - writeSigningKeys(tempRoot.current, []); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - const [, payload] = values.anonKey.split("."); - expect(JSON.parse(Buffer.from(payload ?? "", "base64url").toString())).toMatchObject({ - iss: "supabase-demo", - }); - }); + it.live("throws a Go-worded error when the signing keys file does not exist", () => + Effect.gen(function* () { + const config = baseConfig({ auth: { signing_keys_path: "missing.json" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("failed to read signing keys: "); + }).pipe(Effect.provide(testLayer)), + ); - it("throws a Go-worded error when the signing keys file does not exist", () => { - const config = baseConfig({ auth: { signing_keys_path: "missing.json" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "failed to read signing keys: ", - ); - }); + it.live("throws a Go-worded error when the signing keys file is malformed JSON", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const supabaseDir = path.join(tempRoot.current, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString(path.join(supabaseDir, "signing_keys.json"), "not valid json"); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain('failed to decode signing keys: JSON Parse error: Unexpected identifier "not"'); + }).pipe(Effect.provide(testLayer)), + ); - it("throws a Go-worded error when the signing keys file is malformed JSON", () => { - const supabaseDir = join(tempRoot.current, "supabase"); - mkdirSync(supabaseDir, { recursive: true }); - writeFileSync(join(supabaseDir, "signing_keys.json"), "not valid json"); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - 'failed to decode signing keys: JSON Parse error: Unexpected identifier "not"', - ); - }); + it.live("keeps a leading BOM in the signing keys file, so the file fails to decode", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const supabaseDir = path.join(tempRoot.current, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString( + path.join(supabaseDir, "signing_keys.json"), + `\uFEFF${yield* encodeJson([generateRsaJwk()])}`, + ); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("failed to decode signing keys"); + }).pipe(Effect.provide(testLayer)), + ); - it("throws when the first key uses an unsupported algorithm", () => { - writeSigningKeys(tempRoot.current, [{ ...generateRsaJwk(), alg: "RS512" }]); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "unsupported algorithm: RS512", - ); - }); + it.live("throws when the first key uses an unsupported algorithm", () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, [{ ...generateRsaJwk(), alg: "RS512" }]); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("unsupported algorithm: RS512"); + }).pipe(Effect.provide(testLayer)), + ); it("skips reading a missing signing_keys_path when auth is disabled", () => { const config = baseConfig({ auth: { enabled: false, signing_keys_path: "missing.json" }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); - it("skips reading a malformed signing_keys_path when auth is disabled, but still signs asymmetrically with the default key", async () => { - const supabaseDir = join(tempRoot.current, "supabase"); - mkdirSync(supabaseDir, { recursive: true }); - writeFileSync(join(supabaseDir, "signing_keys.json"), "not valid json"); - const config = baseConfig({ - auth: { enabled: false, signing_keys_path: "signing_keys.json" }, - }); - const values = resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - // Disabled auth with a configured signing-keys path still signs with the default ES256 key, - // not HMAC — signing depends on whether keys were loaded, not on auth being enabled. - const publicKey = await importJWK( - { ...DEFAULT_SIGNING_KEY, d: undefined, key_ops: undefined }, - "ES256", - ); - const { payload, protectedHeader } = await jwtVerify(values.anonKey, publicKey); - expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); - expect(protectedHeader).toMatchObject({ alg: "ES256", kid: DEFAULT_SIGNING_KEY.kid }); - }); + it.live( + "skips reading a malformed signing_keys_path when auth is disabled, but still signs asymmetrically with the default key", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const supabaseDir = path.join(tempRoot.current, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString(path.join(supabaseDir, "signing_keys.json"), "not valid json"); + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "signing_keys.json" }, + }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + // Disabled auth with a configured signing-keys path still signs with the default ES256 key, + // not HMAC — signing depends on whether keys were loaded, not on auth being enabled. + const publicKey = yield* Effect.promise(() => + importJWK({ ...DEFAULT_SIGNING_KEY, d: undefined, key_ops: undefined }, "ES256"), + ); + const { payload, protectedHeader } = yield* Effect.promise(() => + jwtVerify(values.anonKey, publicKey), + ); + expect(payload).toMatchObject({ iss: "supabase-demo", role: "anon" }); + expect(protectedHeader).toMatchObject({ alg: "ES256", kid: DEFAULT_SIGNING_KEY.kid }); + }).pipe(Effect.provide(testLayer)), + ); describe("SUPABASE_AUTH_ENABLED env override", () => { // Reads the post-override `auth.enabled` value, not raw TOML, so an env-only disable/enable // still gates whether `signing_keys_path` is read. afterEach(() => { - delete process.env["SUPABASE_AUTH_ENABLED"]; + delete testEnv["SUPABASE_AUTH_ENABLED"]; }); it("skips reading a missing signing_keys_path when auth is disabled only via env", () => { - process.env["SUPABASE_AUTH_ENABLED"] = "false"; + testEnv["SUPABASE_AUTH_ENABLED"] = "false"; const config = baseConfig({ auth: { enabled: true, signing_keys_path: "missing.json" }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); - it("reads signing_keys_path when auth is enabled only via env despite TOML saying disabled", async () => { - process.env["SUPABASE_AUTH_ENABLED"] = "true"; - const jwk = generateRsaJwk(); - writeSigningKeys(tempRoot.current, [jwk]); - const config = baseConfig({ - auth: { enabled: false, signing_keys_path: "signing_keys.json" }, - }); - const values = resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); - expect(values.anonKey.split(".")).toHaveLength(3); - }); + it.live( + "reads signing_keys_path when auth is enabled only via env despite TOML saying disabled", + () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_ENABLED"] = "true"; + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "signing_keys.json" }, + }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + expect(values.anonKey.split(".")).toHaveLength(3); + }).pipe(Effect.provide(testLayer)), + ); it("rejects a malformed override instead of falling back to the configured value", () => { - process.env["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; const config = baseConfig({ auth: { enabled: false, signing_keys_path: "missing.json" }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))).toThrow( InvalidBoolEnvOverrideError, ); }); @@ -2080,34 +2205,34 @@ describe("resolveLocalConfigValues", () => { // mechanics are tested here. describe("SUPABASE_AUTH_ENABLED / SUPABASE_AUTH_SITE_URL env overrides", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_ENABLED"]; - delete process.env["SUPABASE_AUTH_SITE_URL"]; + delete testEnv["SUPABASE_AUTH_ENABLED"]; + delete testEnv["SUPABASE_AUTH_SITE_URL"]; }); it("rejects an empty site_url when auth is enabled only via env", () => { - process.env["SUPABASE_AUTH_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_ENABLED"] = "true"; const config = baseConfig({ auth: { enabled: false, site_url: "" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: auth.site_url", ); }); it("does not throw when auth is disabled only via env, however empty site_url is", () => { - process.env["SUPABASE_AUTH_ENABLED"] = "false"; + testEnv["SUPABASE_AUTH_ENABLED"] = "false"; const config = baseConfig({ auth: { enabled: true, site_url: "" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("accepts an env-provided site_url overriding an empty config.toml value", () => { - process.env["SUPABASE_AUTH_SITE_URL"] = "http://localhost:4000"; + testEnv["SUPABASE_AUTH_SITE_URL"] = "http://localhost:4000"; const config = baseConfig({ auth: { enabled: true, site_url: "" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("exposes the overridden site_url on the returned values, not just for validation", () => { - process.env["SUPABASE_AUTH_SITE_URL"] = "http://localhost:4000"; + testEnv["SUPABASE_AUTH_SITE_URL"] = "http://localhost:4000"; const config = baseConfig({ auth: { enabled: true, site_url: "http://127.0.0.1:3000" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.authSiteUrl).toBe("http://localhost:4000"); }); }); @@ -2127,21 +2252,21 @@ describe("resolveLocalConfigValues", () => { "SUPABASE_AUTH_PASSWORD_REQUIREMENTS", ]; afterEach(() => { - for (const key of AUTH_SCALAR_ENV_KEYS) delete process.env[key]; + for (const key of AUTH_SCALAR_ENV_KEYS) delete testEnv[key]; }); it("overrides every flat auth.* scalar GoTrue needs, not just the ones Validate checks", () => { - process.env["SUPABASE_AUTH_JWT_ISSUER"] = "https://issuer.example.com"; - process.env["SUPABASE_AUTH_JWT_EXPIRY"] = "7200"; - process.env["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = + testEnv["SUPABASE_AUTH_JWT_ISSUER"] = "https://issuer.example.com"; + testEnv["SUPABASE_AUTH_JWT_EXPIRY"] = "7200"; + testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = "https://a.example.com,https://b.example.com"; - process.env["SUPABASE_AUTH_ENABLE_SIGNUP"] = "false"; - process.env["SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS"] = "true"; - process.env["SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION"] = "false"; - process.env["SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL"] = "20"; - process.env["SUPABASE_AUTH_ENABLE_MANUAL_LINKING"] = "true"; - process.env["SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH"] = "12"; - process.env["SUPABASE_AUTH_PASSWORD_REQUIREMENTS"] = "lower_upper_letters_digits"; + testEnv["SUPABASE_AUTH_ENABLE_SIGNUP"] = "false"; + testEnv["SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS"] = "true"; + testEnv["SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION"] = "false"; + testEnv["SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL"] = "20"; + testEnv["SUPABASE_AUTH_ENABLE_MANUAL_LINKING"] = "true"; + testEnv["SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH"] = "12"; + testEnv["SUPABASE_AUTH_PASSWORD_REQUIREMENTS"] = "lower_upper_letters_digits"; const config = baseConfig({ auth: { @@ -2156,7 +2281,7 @@ describe("resolveLocalConfigValues", () => { password_requirements: "", }, }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.authJwtIssuer).toBe("https://issuer.example.com"); expect(values.authJwtExpiry).toBe(7200); @@ -2174,9 +2299,9 @@ describe("resolveLocalConfigValues", () => { }); it("rejects an unrecognized SUPABASE_AUTH_PASSWORD_REQUIREMENTS override, matching Go's UnmarshalText", () => { - process.env["SUPABASE_AUTH_PASSWORD_REQUIREMENTS"] = "bogus"; + testEnv["SUPABASE_AUTH_PASSWORD_REQUIREMENTS"] = "bogus"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Invalid auth.password_requirements: bogus", ); }); @@ -2187,43 +2312,43 @@ describe("resolveLocalConfigValues", () => { describe("auth.captcha env overrides", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"]; - delete process.env["SUPABASE_AUTH_CAPTCHA_PROVIDER"]; - delete process.env["SUPABASE_AUTH_CAPTCHA_SECRET"]; + delete testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"]; + delete testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"]; + delete testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"]; }); it("rejects a captcha section enabled only via env with no provider", () => { - process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; const config = baseConfig({ auth: { captcha: { enabled: false } } }); const document = { auth: { captcha: { enabled: false } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("Missing required field in config: auth.captcha.provider"); }); it("does not throw when an incomplete enabled captcha section is disabled only via env", () => { - process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "false"; + testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "false"; const config = baseConfig({ auth: { captcha: { enabled: true } } }); const document = { auth: { captcha: { enabled: true } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("accepts env-provided provider/secret overriding an enabled captcha section", () => { - process.env["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "hcaptcha"; - process.env["SUPABASE_AUTH_CAPTCHA_SECRET"] = "shh"; + testEnv["SUPABASE_AUTH_CAPTCHA_PROVIDER"] = "hcaptcha"; + testEnv["SUPABASE_AUTH_CAPTCHA_SECRET"] = "shh"; const config = baseConfig({ auth: { captcha: { enabled: true } } }); const document = { auth: { captcha: { enabled: true } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("does not synthesize a captcha section purely from an env override when [auth.captcha] is absent", () => { - process.env["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_CAPTCHA_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); @@ -2231,45 +2356,44 @@ describe("resolveLocalConfigValues", () => { // `auth.passkey`/`auth.webauthn` have no decoded-schema presence signal, so these tests thread // a raw `document` object through explicitly instead of relying on `baseConfig`. afterEach(() => { - delete process.env["SUPABASE_AUTH_PASSKEY_ENABLED"]; - delete process.env["SUPABASE_AUTH_WEBAUTHN_RP_ID"]; - delete process.env["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"]; + delete testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"]; + delete testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"]; + delete testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"]; }); it("rejects a passkey section enabled only via env with no [auth.webauthn] section", () => { - process.env["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; const config = baseConfig(); const document = { auth: { passkey: { enabled: false } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow( "Missing required config section: auth.webauthn (required when auth.passkey.enabled is true)", ); }); it("accepts env-provided rp_id/rp_origins overriding an incomplete [auth.webauthn] section", () => { - process.env["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; - process.env["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; - process.env["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = - "http://localhost:3000,http://localhost:3001"; + testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; + testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = "http://localhost:3000,http://localhost:3001"; const config = baseConfig(); const document = { auth: { passkey: { enabled: false }, webauthn: {} } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("does not synthesize a passkey section purely from an env override when [auth.passkey] is absent from the document", () => { - process.env["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("throws on an unparsable raw auth.passkey.enabled string instead of silently disabling it", () => { const config = baseConfig(); const document = { auth: { passkey: { enabled: "not-a-bool" } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow('cannot parse "not-a-bool" as a bool'); }); }); @@ -2278,85 +2402,85 @@ describe("resolveLocalConfigValues", () => { // The hook schema always decodes a default `{ enabled: false }` regardless of file presence, // so presence here is read from the raw `document`, not the decoded `config`. afterEach(() => { - delete process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"]; - delete process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_URI"]; - delete process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_SECRETS"]; + delete testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"]; + delete testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_URI"]; + delete testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_SECRETS"]; }); it("rejects a hook section enabled only via env with no uri", () => { - process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"] = "true"; const config = baseConfig(); const document = { auth: { hook: { send_email: { enabled: false } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("Missing required field in config: auth.hook.send_email.uri"); }); it("accepts an env-provided uri overriding a TOML-enabled hook missing its uri", () => { - process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_URI"] = "pg-functions://postgres/auth/hook"; + testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_URI"] = "pg-functions://postgres/auth/hook"; const config = baseConfig({ auth: { hook: { send_email: { enabled: true } } } }); const document = { auth: { hook: { send_email: { enabled: true } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("does not synthesize a hook enablement purely from an env override when the section is absent from the document", () => { - process.env["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_HOOK_SEND_EMAIL_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); describe("auth.email.smtp env overrides", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"]; - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_PORT"]; - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"]; - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"]; - delete process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_PORT"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"]; + delete testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"]; }); it("rejects an smtp section enabled only via env with no host", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "true"; const config = baseConfig(); const document = { auth: { email: { smtp: { enabled: false } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("Missing required field in config: auth.email.smtp.host"); }); it("accepts env-provided host/port/user/pass/admin_email overriding an enabled-but-incomplete smtp section", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.example.com"; - process.env["SUPABASE_AUTH_EMAIL_SMTP_PORT"] = "587"; - process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "user"; - process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "pass"; - process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "admin@example.com"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.example.com"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_PORT"] = "587"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "user"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "pass"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "admin@example.com"; const config = baseConfig(); const document = { auth: { email: { smtp: { enabled: true } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("rejects an invalid SUPABASE_AUTH_EMAIL_SMTP_PORT override", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.example.com"; - process.env["SUPABASE_AUTH_EMAIL_SMTP_PORT"] = "not-a-port"; - process.env["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "user"; - process.env["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "pass"; - process.env["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "admin@example.com"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_HOST"] = "smtp.example.com"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_PORT"] = "not-a-port"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_USER"] = "user"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_PASS"] = "pass"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_ADMIN_EMAIL"] = "admin@example.com"; const config = baseConfig(); const document = { auth: { email: { smtp: { enabled: true } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow(InvalidPortEnvOverrideError); }); it("does not synthesize an smtp section purely from an env override when [auth.email.smtp] is absent from the document", () => { - process.env["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_EMAIL_SMTP_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); @@ -2364,29 +2488,29 @@ describe("resolveLocalConfigValues", () => { // These are plain value-typed fields with no presence gate, unlike hooks/smtp above — they're // overridable unconditionally. afterEach(() => { - delete process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"]; - delete process.env["SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED"]; + delete testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"]; + delete testEnv["SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED"]; }); it("rejects an env-enabled enroll factor left at its TOML-decoded verify default", () => { - process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Invalid MFA config: auth.mfa.totp.enroll_enabled requires verify_enabled", ); }); it("accepts an env-enabled enroll factor when verify is also env-enabled", () => { - process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "true"; - process.env["SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_MFA_TOTP_VERIFY_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("rejects a malformed SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED override", () => { - process.env["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_MFA_TOTP_ENROLL_ENABLED"] = "not-a-bool"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( InvalidBoolEnvOverrideError, ); }); @@ -2396,71 +2520,85 @@ describe("resolveLocalConfigValues", () => { // Same as auth.mfa above — including workos, whose default template omits the whole section // yet leaves it still unconditionally overridable. afterEach(() => { - delete process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"]; - delete process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"]; + delete testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"]; + delete testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"]; }); it("rejects a third-party provider enabled only via env with no required field configured", () => { - process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Invalid config: auth.third_party.firebase is enabled but without a project_id.", ); }); it("accepts an env-provided project_id overriding a TOML-enabled firebase provider", () => { - process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"] = "my-project"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"] = "my-project"; const config = baseConfig({ auth: { third_party: { firebase: { enabled: true } } } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); it("does not enable a third-party provider purely from a required-field env override", () => { - process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"] = "my-project"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID"] = "my-project"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); describe("auth.email.template/notification (content_path validation)", () => { const tempRoot = useTempWorkdir("supabase-email-templates-test-"); - it("rejects a template content_path pointing at a missing file", () => { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "missing-invite.html" } } }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "Invalid config for auth.email.template.invite.content_path: ", - ); - }); + it.live("rejects a template content_path pointing at a missing file", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "missing-invite.html" } } }, + }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("Invalid config for auth.email.template.invite.content_path: "); + }).pipe(Effect.provide(testLayer)), + ); - it("rejects an absolute template content_path outside the project root", () => { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "/etc/hosts" } } }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - 'Invalid config for auth.email.template.invite.content_path: "/etc/hosts" resolves outside the project root', - ); - }); + it.live("rejects an absolute template content_path outside the project root", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "/etc/hosts" } } }, + }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain( + 'Invalid config for auth.email.template.invite.content_path: "/etc/hosts" resolves outside the project root', + ); + }).pipe(Effect.provide(testLayer)), + ); - it("resolves a relative template content_path against the workdir itself, not /supabase", () => { - writeFileSync(join(tempRoot.current, "invite.html"), ""); - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "invite.html" } } }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); - }); + it.live( + "resolves a relative template content_path against the workdir itself, not /supabase", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.writeFileString(path.join(tempRoot.current, "invite.html"), ""); + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "invite.html" } } }, + }, + }); + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + }).pipe(Effect.provide(testLayer)), + ); it("does not throw a template with no content_path configured", () => { const config = baseConfig({ @@ -2470,44 +2608,53 @@ describe("resolveLocalConfigValues", () => { email: { template: { invite: {} } }, }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); - it("rejects an enabled notification content_path pointing at a missing file", () => { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { password_changed: { enabled: true, content_path: "missing.html" } }, + it.live("rejects an enabled notification content_path pointing at a missing file", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { password_changed: { enabled: true, content_path: "missing.html" } }, + }, }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "Invalid config for auth.email.notification.password_changed.content_path: ", - ); - }); + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("Invalid config for auth.email.notification.password_changed.content_path: "); + }).pipe(Effect.provide(testLayer)), + ); - it("resolves a relative notification content_path against the workdir", () => { - const templateDir = join(tempRoot.current, "supabase", "templates"); - mkdirSync(templateDir, { recursive: true }); - writeFileSync(join(templateDir, "pw-changed.html"), ""); - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { - password_changed: { - enabled: true, - content_path: "supabase/templates/pw-changed.html", + it.live("resolves a relative notification content_path against the workdir", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const templateDir = path.join(tempRoot.current, "supabase", "templates"); + yield* fs.makeDirectory(templateDir, { recursive: true }); + yield* fs.writeFileString(path.join(templateDir, "pw-changed.html"), ""); + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { + password_changed: { + enabled: true, + content_path: "supabase/templates/pw-changed.html", + }, }, }, }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); - }); + }); + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + }).pipe(Effect.provide(testLayer)), + ); it("does not throw a disabled notification's missing content_path", () => { const config = baseConfig({ @@ -2521,14 +2668,18 @@ describe("resolveLocalConfigValues", () => { }, }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); it("does not throw a missing template content_path when auth is disabled", () => { const config = baseConfig({ auth: { enabled: false, email: { template: { invite: { content_path: "missing.html" } } } }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); it("rejects a template content key present without content_path", () => { @@ -2540,9 +2691,11 @@ describe("resolveLocalConfigValues", () => { }, }); expect(() => - resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current, undefined, { - auth: { email: { template: { invite: { content: "Hi" } } } }, - }), + run( + resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current, undefined, { + auth: { email: { template: { invite: { content: "Hi" } } } }, + }), + ), ).toThrow( "Invalid config for auth.email.template.invite.content: please use content_path instead", ); @@ -2555,44 +2708,51 @@ describe("resolveLocalConfigValues", () => { const tempRoot = useTempWorkdir("supabase-email-template-env-test-"); afterEach(() => { - delete process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"]; - delete process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"]; - delete process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"]; - delete process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"]; - delete process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"]; - }); - - it("lets an env-provided template content_path override a missing TOML content_path", () => { - writeFileSync(join(tempRoot.current, "invite.html"), ""); - process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "invite.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: {} } }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); - }); + delete testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"]; + delete testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"]; + delete testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"]; + delete testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"]; + delete testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"]; + }); + + it.live("lets an env-provided template content_path override a missing TOML content_path", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.writeFileString(path.join(tempRoot.current, "invite.html"), ""); + testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "invite.html"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + }).pipe(Effect.provide(testLayer)), + ); - it("rejects a notification enabled only via env with a missing content_path file", () => { - process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "true"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { password_changed: { enabled: false, content_path: "missing.html" } }, + it.live("rejects a notification enabled only via env with a missing content_path file", () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "true"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { password_changed: { enabled: false, content_path: "missing.html" } }, + }, }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "Invalid config for auth.email.notification.password_changed.content_path: ", - ); - }); + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("Invalid config for auth.email.notification.password_changed.content_path: "); + }).pipe(Effect.provide(testLayer)), + ); it("does not validate a notification disabled only via env despite a TOML-enabled section", () => { - process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "false"; + testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "false"; const config = baseConfig({ auth: { enabled: true, @@ -2602,25 +2762,36 @@ describe("resolveLocalConfigValues", () => { }, }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); - it("lets an env-provided notification content_path override a missing TOML content_path", () => { - writeFileSync(join(tempRoot.current, "pw-changed.html"), ""); - process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"] = - "pw-changed.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { notification: { password_changed: { enabled: true } } }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); - }); + it.live( + "lets an env-provided notification content_path override a missing TOML content_path", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.writeFileString( + path.join(tempRoot.current, "pw-changed.html"), + "", + ); + testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"] = + "pw-changed.html"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { notification: { password_changed: { enabled: true } } }, + }, + }); + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + }).pipe(Effect.provide(testLayer)), + ); it("rejects a template _CONTENT env override with no content_path configured", () => { - process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"] = "Hi"; + testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"] = "Hi"; const config = baseConfig({ auth: { enabled: true, @@ -2628,13 +2799,13 @@ describe("resolveLocalConfigValues", () => { email: { template: { invite: {} } }, }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))).toThrow( "Invalid config for auth.email.template.invite.content: please use content_path instead", ); }); it("rejects an enabled notification's _CONTENT env override with no content_path configured", () => { - process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"] = "Hi"; + testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"] = "Hi"; const config = baseConfig({ auth: { enabled: true, @@ -2642,13 +2813,13 @@ describe("resolveLocalConfigValues", () => { email: { notification: { password_changed: { enabled: true } } }, }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))).toThrow( "Invalid config for auth.email.notification.password_changed.content: please use content_path instead", ); }); it("does not validate a disabled notification's _CONTENT env override", () => { - process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"] = "Hi"; + testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT"] = "Hi"; const config = baseConfig({ auth: { enabled: true, @@ -2656,88 +2827,112 @@ describe("resolveLocalConfigValues", () => { email: { notification: { password_changed: { enabled: false } } }, }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); - }); - - it("lets a simultaneous template _CONTENT_PATH env override win over a _CONTENT env override", () => { - writeFileSync(join(tempRoot.current, "invite.html"), ""); - process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"] = "Hi"; - process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "invite.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: {} } }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); - }); - - it("preserves a remote block's valid template content_path over a missing-file ambient override", () => { - writeFileSync(join(tempRoot.current, "invite.html"), ""); - process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "missing.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "invite.html" } } }, - }, - }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["auth.email.template.invite.content_path"]), - ), + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), ).not.toThrow(); }); - it("still applies a template _CONTENT_PATH override to a missing file when no remote block matched", () => { - process.env["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "missing.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: { content_path: "invite.html" } } }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "Invalid config for auth.email.template.invite.content_path: ", - ); - }); + it.live( + "lets a simultaneous template _CONTENT_PATH env override win over a _CONTENT env override", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.writeFileString(path.join(tempRoot.current, "invite.html"), ""); + testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT"] = "Hi"; + testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "invite.html"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + }).pipe(Effect.provide(testLayer)), + ); - it("preserves a remote block's valid notification content_path over a missing-file ambient override", () => { - writeFileSync(join(tempRoot.current, "pw-changed.html"), ""); - process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"] = - "missing.html"; - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { - password_changed: { enabled: true, content_path: "pw-changed.html" }, + it.live( + "preserves a remote block's valid template content_path over a missing-file ambient override", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.writeFileString(path.join(tempRoot.current, "invite.html"), ""); + testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "missing.html"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "invite.html" } } }, }, - }, - }, - }); - expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["auth.email.notification.password_changed.content_path"]), - ), - ).not.toThrow(); - }); + }); + yield* resolveLocalConfigValues( + config, + "127.0.0.1", + tempRoot.current, + undefined, + undefined, + new Set(["auth.email.template.invite.content_path"]), + ); + }).pipe(Effect.provide(testLayer)), + ); + + it.live( + "still applies a template _CONTENT_PATH override to a missing file when no remote block matched", + () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_EMAIL_TEMPLATE_INVITE_CONTENT_PATH"] = "missing.html"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "invite.html" } } }, + }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("Invalid config for auth.email.template.invite.content_path: "); + }).pipe(Effect.provide(testLayer)), + ); + + it.live( + "preserves a remote block's valid notification content_path over a missing-file ambient override", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.writeFileString( + path.join(tempRoot.current, "pw-changed.html"), + "", + ); + testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_CONTENT_PATH"] = + "missing.html"; + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { + notification: { + password_changed: { enabled: true, content_path: "pw-changed.html" }, + }, + }, + }, + }); + yield* resolveLocalConfigValues( + config, + "127.0.0.1", + tempRoot.current, + undefined, + undefined, + new Set(["auth.email.notification.password_changed.content_path"]), + ); + }).pipe(Effect.provide(testLayer)), + ); it("suppresses a malformed ambient notification _ENABLED when a remote block already set enabled", () => { - process.env["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_EMAIL_NOTIFICATION_PASSWORD_CHANGED_ENABLED"] = "not-a-bool"; const config = baseConfig({ auth: { enabled: true, @@ -2746,13 +2941,15 @@ describe("resolveLocalConfigValues", () => { }, }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["auth.email.notification.password_changed.enabled"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + tempRoot.current, + undefined, + undefined, + new Set(["auth.email.notification.password_changed.enabled"]), + ), ), ).not.toThrow(); }); @@ -2768,7 +2965,7 @@ describe("resolveLocalConfigValues", () => { const config = baseConfig(); const document = { auth: { external: { custom: { enabled: true } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("Missing required field in config: auth.external.custom.client_id"); }); @@ -2778,7 +2975,7 @@ describe("resolveLocalConfigValues", () => { auth: { external: { custom: { enabled: true, client_id: "abc" } } }, }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("Missing required field in config: auth.external.custom.secret"); }); @@ -2788,7 +2985,7 @@ describe("resolveLocalConfigValues", () => { auth: { external: { apple: { enabled: true, client_id: "abc" } } }, }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); @@ -2796,7 +2993,7 @@ describe("resolveLocalConfigValues", () => { const config = baseConfig(); const document = { auth: { external: { slack: { enabled: true } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); @@ -2804,46 +3001,46 @@ describe("resolveLocalConfigValues", () => { const config = baseConfig(); const document = { auth: { external: { custom: { enabled: false } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("skips the check entirely when no document is threaded through", () => { const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); describe("auth.external env overrides", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"]; - delete process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_CLIENT_ID"]; - delete process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_SECRET"]; + delete testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"]; + delete testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_CLIENT_ID"]; + delete testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_SECRET"]; }); it("rejects a provider enabled only via env with no client_id", () => { - process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"] = "true"; const config = baseConfig(); const document = { auth: { external: { custom: { enabled: false } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("Missing required field in config: auth.external.custom.client_id"); }); it("accepts env-provided client_id/secret overriding a TOML-enabled provider missing both", () => { - process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_CLIENT_ID"] = "abc"; - process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_SECRET"] = "shh"; + testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_CLIENT_ID"] = "abc"; + testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_SECRET"] = "shh"; const config = baseConfig(); const document = { auth: { external: { custom: { enabled: true } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("does not synthesize a provider purely from an env override when the section is absent from the document", () => { - process.env["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_EXTERNAL_CUSTOM_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); @@ -2852,42 +3049,42 @@ describe("resolveLocalConfigValues", () => { // messagebird, textlocal, vonage), re-run here against the raw document with env overrides // applied. afterEach(() => { - delete process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"]; - delete process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; - delete process.env["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"]; - delete process.env["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"]; - delete process.env["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"]; + delete testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"]; + delete testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; + delete testEnv["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"]; + delete testEnv["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"]; + delete testEnv["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"]; }); it("rejects a provider enabled only via env with missing required fields", () => { - process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; const config = baseConfig(); const document = { auth: { sms: { twilio: { enabled: false } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("Missing required field in config: auth.sms.twilio.account_sid"); }); it("accepts env-provided credentials overriding a TOML-enabled provider missing them", () => { - process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "AC123"; - process.env["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"] = "MG123"; - process.env["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"] = "tok"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "AC123"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"] = "MG123"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"] = "tok"; const config = baseConfig(); const document = { auth: { sms: { twilio: { enabled: true } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("only validates the first enabled provider in Go's fixed priority order", () => { - process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "false"; - process.env["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "false"; + testEnv["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"] = "true"; const config = baseConfig(); const document = { auth: { sms: { twilio: { enabled: true }, messagebird: { enabled: false } } }, }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("Missing required field in config: auth.sms.messagebird.originator"); }); @@ -2895,19 +3092,19 @@ describe("resolveLocalConfigValues", () => { // Unlike the other 4 providers, twilio's presence isn't gated on the document — the default // config always registers `auth.sms.twilio.*`, so enabling only via env still fails // validation instead of doing nothing. - process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Missing required field in config: auth.sms.twilio.account_sid", ); }); it("resolves a fully env-only twilio configuration with no auth.sms.twilio document section", () => { - process.env["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; - process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "AC123"; - process.env["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"] = "MG123"; - process.env["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"] = "tok"; - const resolved = resolveAuthSms(undefined, baseConfig().auth.sms, undefined); + testEnv["SUPABASE_AUTH_SMS_TWILIO_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "AC123"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_MESSAGE_SERVICE_SID"] = "MG123"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN"] = "tok"; + const resolved = run(resolveAuthSms(undefined, baseConfig().auth.sms, undefined)); expect(resolved.twilio.enabled).toBe(true); expect(resolved.twilio.account_sid).toBe("AC123"); expect(resolved.twilio.message_service_sid).toBe("MG123"); @@ -2917,34 +3114,34 @@ describe("resolveLocalConfigValues", () => { it("still does not synthesize messagebird purely from an env override when the section is absent from the document", () => { // Unlike twilio, messagebird has no entry in the default config template, so an absent // section genuinely means it was never registered. - process.env["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"] = "true"; + testEnv["SUPABASE_AUTH_SMS_MESSAGEBIRD_ENABLED"] = "true"; const config = baseConfig(); - const resolved = resolveAuthSms(undefined, config.auth.sms, undefined); + const resolved = run(resolveAuthSms(undefined, config.auth.sms, undefined)); expect(resolved.messagebird.enabled).toBe(false); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).not.toThrow(); }); }); describe("resolveAuthSms (top-level scalars)", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; - delete process.env["SUPABASE_AUTH_SMS_ENABLE_CONFIRMATIONS"]; - delete process.env["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; - delete process.env["SUPABASE_AUTH_SMS_TEMPLATE"]; + delete testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; + delete testEnv["SUPABASE_AUTH_SMS_ENABLE_CONFIRMATIONS"]; + delete testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; + delete testEnv["SUPABASE_AUTH_SMS_TEMPLATE"]; }); it("overrides enable_signup/enable_confirmations/max_frequency/template with no presence gate", () => { - process.env["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "true"; - process.env["SUPABASE_AUTH_SMS_ENABLE_CONFIRMATIONS"] = "true"; - process.env["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "10s"; - process.env["SUPABASE_AUTH_SMS_TEMPLATE"] = "Your OTP is {{ .Code }}"; + testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "true"; + testEnv["SUPABASE_AUTH_SMS_ENABLE_CONFIRMATIONS"] = "true"; + testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "10s"; + testEnv["SUPABASE_AUTH_SMS_TEMPLATE"] = "Your OTP is {{ .Code }}"; // A provider must be enabled, or `enable_signup` gets downgraded to false regardless of the // override. const configured = { ...baseConfig().auth.sms, twilio: { ...baseConfig().auth.sms.twilio, enabled: true }, }; - const resolved = resolveAuthSms(undefined, configured, undefined); + const resolved = run(resolveAuthSms(undefined, configured, undefined)); expect(resolved.enable_signup).toBe(true); expect(resolved.enable_confirmations).toBe(true); expect(resolved.max_frequency).toBe("10s"); @@ -2958,7 +3155,7 @@ describe("resolveLocalConfigValues", () => { max_frequency: "5s", twilio: { ...baseConfig().auth.sms.twilio, enabled: true }, }; - const resolved = resolveAuthSms(undefined, configured, undefined); + const resolved = run(resolveAuthSms(undefined, configured, undefined)); expect(resolved.enable_signup).toBe(true); expect(resolved.max_frequency).toBe("5s"); }); @@ -2966,18 +3163,18 @@ describe("resolveLocalConfigValues", () => { describe("resolveAuthSms (disables phone login with no provider enabled)", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; + delete testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; }); it("downgrades enable_signup to false when configured true with no provider enabled", () => { const configured = { ...baseConfig().auth.sms, enable_signup: true }; - const resolved = resolveAuthSms(undefined, configured, undefined); + const resolved = run(resolveAuthSms(undefined, configured, undefined)); expect(resolved.enable_signup).toBe(false); }); it("downgrades an env-overridden enable_signup to false with no provider enabled", () => { - process.env["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "true"; - const resolved = resolveAuthSms(undefined, baseConfig().auth.sms, undefined); + testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "true"; + const resolved = run(resolveAuthSms(undefined, baseConfig().auth.sms, undefined)); expect(resolved.enable_signup).toBe(false); }); @@ -2987,214 +3184,201 @@ describe("resolveLocalConfigValues", () => { enable_signup: true, vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, }; - const resolved = resolveAuthSms(undefined, configured, undefined); + const resolved = run(resolveAuthSms(undefined, configured, undefined)); expect(resolved.enable_signup).toBe(true); }); it("leaves enable_signup at false when already false with no provider enabled", () => { - const resolved = resolveAuthSms(undefined, baseConfig().auth.sms, undefined); + const resolved = run(resolveAuthSms(undefined, baseConfig().auth.sms, undefined)); expect(resolved.enable_signup).toBe(false); }); }); describe("resolveAuthSms — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; - delete process.env["SUPABASE_AUTH_SMS_VONAGE_ENABLED"]; - delete process.env["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"]; + delete testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"]; + delete testEnv["SUPABASE_AUTH_SMS_VONAGE_ENABLED"]; + delete testEnv["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"]; }); it("suppresses a malformed SUPABASE_AUTH_SMS_ENABLE_SIGNUP when a remote block already set auth.sms.enable_signup", () => { - process.env["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; const configured = { ...baseConfig().auth.sms, enable_signup: true, vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, }; expect(() => - resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.enable_signup"])), + run(resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.enable_signup"]))), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_AUTH_SMS_ENABLE_SIGNUP when no remote block matched", () => { - process.env["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; const configured = { ...baseConfig().auth.sms, enable_signup: true, vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, }; - expect(() => resolveAuthSms(undefined, configured, undefined)).toThrow( + expect(() => run(resolveAuthSms(undefined, configured, undefined))).toThrow( 'cannot parse "not-a-bool" as a bool', ); }); it("suppresses a malformed SUPABASE_AUTH_SMS_VONAGE_ENABLED when a remote block already set auth.sms.vonage.enabled", () => { - process.env["SUPABASE_AUTH_SMS_VONAGE_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_SMS_VONAGE_ENABLED"] = "not-a-bool"; const configured = { ...baseConfig().auth.sms, vonage: { ...baseConfig().auth.sms.vonage, enabled: true }, }; expect(() => - resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.vonage.enabled"])), + run(resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.vonage.enabled"]))), ).not.toThrow(); }); it("prefers a remote-set auth.sms.vonage.api_secret over a malformed SUPABASE_AUTH_SMS_VONAGE_API_SECRET", () => { - process.env["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"] = "encrypted:garbage"; + testEnv["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"] = "encrypted:garbage"; const configured = { ...baseConfig().auth.sms, vonage: { ...baseConfig().auth.sms.vonage, enabled: true, api_secret: "remote-secret" }, }; - const resolved = resolveAuthSms( - undefined, - configured, - undefined, - new Set(["auth.sms.vonage.enabled", "auth.sms.vonage.api_secret"]), + const resolved = run( + resolveAuthSms( + undefined, + configured, + undefined, + new Set(["auth.sms.vonage.enabled", "auth.sms.vonage.api_secret"]), + ), ); expect(resolved.vonage.api_secret).toBe("remote-secret"); }); it("still rejects a malformed SUPABASE_AUTH_SMS_VONAGE_API_SECRET when no remote block matched", () => { - process.env["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"] = "encrypted:garbage"; + testEnv["SUPABASE_AUTH_SMS_VONAGE_API_SECRET"] = "encrypted:garbage"; const authDocument = { sms: { vonage: {} } }; const configured = { ...baseConfig().auth.sms, vonage: { ...baseConfig().auth.sms.vonage, enabled: true, api_secret: "remote-secret" }, }; - expect(() => resolveAuthSms(authDocument, configured, undefined)).toThrow( + expect(() => run(resolveAuthSms(authDocument, configured, undefined))).toThrow( "failed to parse config: missing private key", ); }); it("prefers a remote-set auth.sms.twilio.account_sid over a conflicting SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID", () => { - process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "env-sid"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "env-sid"; const configured = { ...baseConfig().auth.sms, twilio: { ...baseConfig().auth.sms.twilio, account_sid: "remote-sid" }, }; - const resolved = resolveAuthSms( - undefined, - configured, - undefined, - new Set(["auth.sms.twilio.account_sid"]), + const resolved = run( + resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.twilio.account_sid"])), ); expect(resolved.twilio.account_sid).toBe("remote-sid"); - delete process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; + delete testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; }); it("still applies SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID when no remote block matched", () => { - process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "env-sid"; + testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"] = "env-sid"; const configured = { ...baseConfig().auth.sms, twilio: { ...baseConfig().auth.sms.twilio, account_sid: "remote-sid" }, }; - const resolved = resolveAuthSms(undefined, configured, undefined); + const resolved = run(resolveAuthSms(undefined, configured, undefined)); expect(resolved.twilio.account_sid).toBe("env-sid"); - delete process.env["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; + delete testEnv["SUPABASE_AUTH_SMS_TWILIO_ACCOUNT_SID"]; }); it("prefers a remote-set auth.sms.vonage.from over a conflicting SUPABASE_AUTH_SMS_VONAGE_FROM", () => { - process.env["SUPABASE_AUTH_SMS_VONAGE_FROM"] = "env-from"; + testEnv["SUPABASE_AUTH_SMS_VONAGE_FROM"] = "env-from"; const authDocument = { sms: { vonage: { from: "remote-from" } } }; const configured = { ...baseConfig().auth.sms, vonage: { ...baseConfig().auth.sms.vonage, from: "remote-from" }, }; - const resolved = resolveAuthSms( - authDocument, - configured, - undefined, - new Set(["auth.sms.vonage.from"]), + const resolved = run( + resolveAuthSms(authDocument, configured, undefined, new Set(["auth.sms.vonage.from"])), ); expect(resolved.vonage.from).toBe("remote-from"); - delete process.env["SUPABASE_AUTH_SMS_VONAGE_FROM"]; + delete testEnv["SUPABASE_AUTH_SMS_VONAGE_FROM"]; }); it("still applies SUPABASE_AUTH_SMS_VONAGE_FROM when no remote block matched", () => { - process.env["SUPABASE_AUTH_SMS_VONAGE_FROM"] = "env-from"; + testEnv["SUPABASE_AUTH_SMS_VONAGE_FROM"] = "env-from"; const authDocument = { sms: { vonage: { from: "remote-from" } } }; const configured = { ...baseConfig().auth.sms, vonage: { ...baseConfig().auth.sms.vonage, from: "remote-from" }, }; - const resolved = resolveAuthSms(authDocument, configured, undefined); + const resolved = run(resolveAuthSms(authDocument, configured, undefined)); expect(resolved.vonage.from).toBe("env-from"); - delete process.env["SUPABASE_AUTH_SMS_VONAGE_FROM"]; + delete testEnv["SUPABASE_AUTH_SMS_VONAGE_FROM"]; }); it("prefers a remote-set auth.sms.vonage.api_key over a conflicting SUPABASE_AUTH_SMS_VONAGE_API_KEY", () => { - process.env["SUPABASE_AUTH_SMS_VONAGE_API_KEY"] = "env-key"; + testEnv["SUPABASE_AUTH_SMS_VONAGE_API_KEY"] = "env-key"; const authDocument = { sms: { vonage: { api_key: "remote-key" } } }; const configured = { ...baseConfig().auth.sms, vonage: { ...baseConfig().auth.sms.vonage, api_key: "remote-key" }, }; - const resolved = resolveAuthSms( - authDocument, - configured, - undefined, - new Set(["auth.sms.vonage.api_key"]), + const resolved = run( + resolveAuthSms(authDocument, configured, undefined, new Set(["auth.sms.vonage.api_key"])), ); expect(resolved.vonage.api_key).toBe("remote-key"); - delete process.env["SUPABASE_AUTH_SMS_VONAGE_API_KEY"]; + delete testEnv["SUPABASE_AUTH_SMS_VONAGE_API_KEY"]; }); it("still applies SUPABASE_AUTH_SMS_VONAGE_API_KEY when no remote block matched", () => { - process.env["SUPABASE_AUTH_SMS_VONAGE_API_KEY"] = "env-key"; + testEnv["SUPABASE_AUTH_SMS_VONAGE_API_KEY"] = "env-key"; const authDocument = { sms: { vonage: { api_key: "remote-key" } } }; const configured = { ...baseConfig().auth.sms, vonage: { ...baseConfig().auth.sms.vonage, api_key: "remote-key" }, }; - const resolved = resolveAuthSms(authDocument, configured, undefined); + const resolved = run(resolveAuthSms(authDocument, configured, undefined)); expect(resolved.vonage.api_key).toBe("env-key"); - delete process.env["SUPABASE_AUTH_SMS_VONAGE_API_KEY"]; + delete testEnv["SUPABASE_AUTH_SMS_VONAGE_API_KEY"]; }); it("prefers a remote-set auth.sms.template over a conflicting SUPABASE_AUTH_SMS_TEMPLATE", () => { - process.env["SUPABASE_AUTH_SMS_TEMPLATE"] = "env template"; + testEnv["SUPABASE_AUTH_SMS_TEMPLATE"] = "env template"; const configured = { ...baseConfig().auth.sms, template: "remote template" }; - const resolved = resolveAuthSms( - undefined, - configured, - undefined, - new Set(["auth.sms.template"]), + const resolved = run( + resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.template"])), ); expect(resolved.template).toBe("remote template"); - delete process.env["SUPABASE_AUTH_SMS_TEMPLATE"]; + delete testEnv["SUPABASE_AUTH_SMS_TEMPLATE"]; }); it("still applies SUPABASE_AUTH_SMS_TEMPLATE when no remote block matched", () => { - process.env["SUPABASE_AUTH_SMS_TEMPLATE"] = "env template"; + testEnv["SUPABASE_AUTH_SMS_TEMPLATE"] = "env template"; const configured = { ...baseConfig().auth.sms, template: "remote template" }; - const resolved = resolveAuthSms(undefined, configured, undefined); + const resolved = run(resolveAuthSms(undefined, configured, undefined)); expect(resolved.template).toBe("env template"); - delete process.env["SUPABASE_AUTH_SMS_TEMPLATE"]; + delete testEnv["SUPABASE_AUTH_SMS_TEMPLATE"]; }); it("prefers a remote-set auth.sms.max_frequency over a conflicting SUPABASE_AUTH_SMS_MAX_FREQUENCY", () => { - process.env["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "5s"; + testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "5s"; const configured = { ...baseConfig().auth.sms, max_frequency: "1m" }; - const resolved = resolveAuthSms( - undefined, - configured, - undefined, - new Set(["auth.sms.max_frequency"]), + const resolved = run( + resolveAuthSms(undefined, configured, undefined, new Set(["auth.sms.max_frequency"])), ); expect(resolved.max_frequency).toBe("1m"); - delete process.env["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; + delete testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; }); it("still applies SUPABASE_AUTH_SMS_MAX_FREQUENCY when no remote block matched", () => { - process.env["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "5s"; + testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"] = "5s"; const configured = { ...baseConfig().auth.sms, max_frequency: "1m" }; - const resolved = resolveAuthSms(undefined, configured, undefined); + const resolved = run(resolveAuthSms(undefined, configured, undefined)); expect(resolved.max_frequency).toBe("5s"); - delete process.env["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; + delete testEnv["SUPABASE_AUTH_SMS_MAX_FREQUENCY"]; }); it("still aborts resolveLocalConfigValues on a malformed SUPABASE_AUTH_SMS_ENABLE_SIGNUP reached via validateAuthSmsProviders, unless remoteOverrideKeys suppresses it", () => { - process.env["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_SMS_ENABLE_SIGNUP"] = "not-a-bool"; const base = baseConfig(); const config: CliConfig = { ...base, @@ -3214,17 +3398,19 @@ describe("resolveLocalConfigValues", () => { }, }, }; - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'cannot parse "not-a-bool" as a bool', ); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.sms.enable_signup"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.sms.enable_signup"]), + ), ), ).not.toThrow(); }); @@ -3233,91 +3419,115 @@ describe("resolveLocalConfigValues", () => { describe("api.tls (cert/key validation)", () => { const tempRoot = useTempWorkdir("supabase-api-tls-test-"); - function writeTlsFile(workdir: string, name: string, contents = "dummy") { - const supabaseDir = join(workdir, "supabase"); - mkdirSync(supabaseDir, { recursive: true }); - writeFileSync(join(supabaseDir, name), contents); - } + const writeTlsFile = Effect.fnUntraced(function* ( + workdir: string, + name: string, + contents = "dummy", + ) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const supabaseDir = path.join(workdir, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString(path.join(supabaseDir, name), contents); + }); it("does not throw when tls.enabled with neither cert_path nor key_path set", () => { const config = baseConfig({ api: { tls: { enabled: true } } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); // The "exactly one of cert/key set" checks live in config-validate.unit.test.ts; the file-read // behavior below is tested here. - it("throws a Go-worded error when the configured cert file does not exist", () => { - writeTlsFile(tempRoot.current, "key.pem"); - const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "missing-cert.pem", key_path: "key.pem" } }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "failed to read TLS cert: ", - ); - }); + it.live("throws a Go-worded error when the configured cert file does not exist", () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "missing-cert.pem", key_path: "key.pem" } }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("failed to read TLS cert: "); + }).pipe(Effect.provide(testLayer)), + ); - it("throws a Go-worded error when the configured key file does not exist", () => { - writeTlsFile(tempRoot.current, "cert.pem"); - const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "missing-key.pem" } }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "failed to read TLS key: ", - ); - }); + it.live("throws a Go-worded error when the configured key file does not exist", () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "cert.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "missing-key.pem" } }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("failed to read TLS key: "); + }).pipe(Effect.provide(testLayer)), + ); - it("succeeds when both cert_path and key_path are readable", () => { - writeTlsFile(tempRoot.current, "cert.pem"); - writeTlsFile(tempRoot.current, "key.pem"); - const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "key.pem" } }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); - }); + it.live("succeeds when both cert_path and key_path are readable", () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "cert.pem"); + yield* writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "key.pem" } }, + }); + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + }).pipe(Effect.provide(testLayer)), + ); - it("resolves cert_path/key_path against /supabase unconditionally, no isAbsolute guard", () => { - writeTlsFile(tempRoot.current, "cert.pem"); - writeTlsFile(tempRoot.current, "key.pem"); - const config = baseConfig({ - api: { - tls: { - enabled: true, - cert_path: "/cert.pem", - key_path: "/key.pem", - }, - }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); - }); + it.live( + "resolves cert_path/key_path against /supabase unconditionally, no isAbsolute guard", + () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "cert.pem"); + yield* writeTlsFile(tempRoot.current, "key.pem"); + const config = baseConfig({ + api: { + tls: { + enabled: true, + cert_path: "/cert.pem", + key_path: "/key.pem", + }, + }, + }); + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current); + }).pipe(Effect.provide(testLayer)), + ); it("skips TLS validation entirely when api is disabled", () => { const config = baseConfig({ api: { enabled: false, tls: { enabled: true, cert_path: "missing-cert.pem" } }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); describe("SUPABASE_API_ENABLED / SUPABASE_API_TLS_ENABLED env overrides", () => { afterEach(() => { - delete process.env["SUPABASE_API_ENABLED"]; - delete process.env["SUPABASE_API_TLS_ENABLED"]; + delete testEnv["SUPABASE_API_ENABLED"]; + delete testEnv["SUPABASE_API_TLS_ENABLED"]; }); it("skips TLS validation when api is disabled only via env", () => { - process.env["SUPABASE_API_ENABLED"] = "false"; + testEnv["SUPABASE_API_ENABLED"] = "false"; const config = baseConfig({ api: { enabled: true, tls: { enabled: true, cert_path: "missing-cert.pem" } }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).not.toThrow(); + expect(() => + run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)), + ).not.toThrow(); }); it("validates TLS when enabled only via env despite TOML saying tls.enabled = false", () => { - process.env["SUPABASE_API_TLS_ENABLED"] = "true"; + testEnv["SUPABASE_API_TLS_ENABLED"] = "true"; const config = baseConfig({ api: { tls: { enabled: false, cert_path: "missing-cert.pem" } }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))).toThrow( "Missing required field in config: api.tls.key_path", ); }); @@ -3369,92 +3579,105 @@ describe("resolveLocalConfigValues — remoteOverrideKeys (linked shadow provisi "SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED", "SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI", ]) { - delete process.env[name]; + delete testEnv[name]; } }); const tempRoot = useTempWorkdir("supabase-remote-signing-keys-test-"); - it("prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", () => { - writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["auth.signing_keys_path"]), - ), - ).not.toThrow(); - }); + it.live( + "prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", + () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); + testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + yield* resolveLocalConfigValues( + config, + "127.0.0.1", + tempRoot.current, + undefined, + undefined, + new Set(["auth.signing_keys_path"]), + ); + }).pipe(Effect.provide(testLayer)), + ); - it("still rejects a missing SUPABASE_AUTH_SIGNING_KEYS_PATH override when no remote block matched", () => { - writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "failed to read signing keys: ", - ); - }); + it.live( + "still rejects a missing SUPABASE_AUTH_SIGNING_KEYS_PATH override when no remote block matched", + () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); + testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("failed to read signing keys: "); + }).pipe(Effect.provide(testLayer)), + ); it("suppresses a malformed SUPABASE_DB_MAJOR_VERSION when a remote block already set db.major_version", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "abc"; + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "abc"; const config = baseConfig({ db: { major_version: 14 } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["db.major_version"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["db.major_version"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_DB_MAJOR_VERSION when no remote block matched", () => { - process.env["SUPABASE_DB_MAJOR_VERSION"] = "abc"; + testEnv["SUPABASE_DB_MAJOR_VERSION"] = "abc"; const config = baseConfig({ db: { major_version: 14 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Invalid db.major_version: abc", ); }); it("prefers a remote-set auth.jwt_secret over a conflicting SUPABASE_AUTH_JWT_SECRET", () => { - process.env["SUPABASE_AUTH_JWT_SECRET"] = "env-supplied-secret-value-1234567890"; + testEnv["SUPABASE_AUTH_JWT_SECRET"] = "env-supplied-secret-value-1234567890"; const config = baseConfig({ auth: { jwt_secret: "remote-supplied-secret-1234567890" } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.jwt_secret"]), - ); - expect(values.jwtSecret).toBe("remote-supplied-secret-1234567890"); - }); - + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.jwt_secret"]), + ), + ); + expect(values.jwtSecret).toBe("remote-supplied-secret-1234567890"); + }); + it("prefers a remote-set db.root_key over a conflicting SUPABASE_DB_ROOT_KEY", () => { - process.env["SUPABASE_DB_ROOT_KEY"] = "env-root-key"; + testEnv["SUPABASE_DB_ROOT_KEY"] = "env-root-key"; const config = baseConfig(); const document = { db: { root_key: "remote-root-key" } }; - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - document, - new Set(["db.root_key"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + document, + new Set(["db.root_key"]), + ), ); expect(values.rootKey).toBe("remote-root-key"); }); it("prefers a remote-set auth.third_party.clerk.domain over a conflicting env override during validation", () => { - process.env["SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED"] = "false"; - process.env["SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN"] = "not-a-clerk-domain"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED"] = "false"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN"] = "not-a-clerk-domain"; const config = baseConfig({ auth: { enabled: true, @@ -3462,26 +3685,28 @@ describe("resolveLocalConfigValues — remoteOverrideKeys (linked shadow provisi }, }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.third_party.clerk.enabled", "auth.third_party.clerk.domain"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.third_party.clerk.enabled", "auth.third_party.clerk.domain"]), + ), ), ).not.toThrow(); }); it("still rejects a conflicting SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN when no remote block matched", () => { - process.env["SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN"] = "not-a-clerk-domain"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN"] = "not-a-clerk-domain"; const config = baseConfig({ auth: { enabled: true, third_party: { clerk: { enabled: true, domain: "clerk.example.com" } }, }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Invalid config: auth.third_party.clerk has invalid domain", ); }); @@ -3489,365 +3714,408 @@ describe("resolveLocalConfigValues — remoteOverrideKeys (linked shadow provisi describe("api.tls.cert_path/key_path — remoteOverrideKeys (review: PRRT_kwDOErm0O86W8ZYk)", () => { const tempRoot = useTempWorkdir("supabase-api-tls-remote-test-"); - function writeTlsFile(workdir: string, name: string, contents = "dummy") { - const supabaseDir = join(workdir, "supabase"); - mkdirSync(supabaseDir, { recursive: true }); - writeFileSync(join(supabaseDir, name), contents); - } - - afterEach(() => { - delete process.env["SUPABASE_API_TLS_CERT_PATH"]; - delete process.env["SUPABASE_API_TLS_KEY_PATH"]; + const writeTlsFile = Effect.fnUntraced(function* ( + workdir: string, + name: string, + contents = "dummy", + ) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const supabaseDir = path.join(workdir, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString(path.join(supabaseDir, name), contents); }); - it("prefers a remote-set api.tls.cert_path/key_path over a conflicting (missing-file) env override", () => { - // The env vars point at files that don't exist; the load only succeeds if the remote-set - // paths win instead. - writeTlsFile(tempRoot.current, "cert.pem"); - writeTlsFile(tempRoot.current, "key.pem"); - process.env["SUPABASE_API_TLS_CERT_PATH"] = "missing-cert.pem"; - process.env["SUPABASE_API_TLS_KEY_PATH"] = "missing-key.pem"; - const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "key.pem" } }, - }); - expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - tempRoot.current, - undefined, - undefined, - new Set(["api.tls.cert_path", "api.tls.key_path"]), - ), - ).not.toThrow(); - }); + afterEach(() => { + delete testEnv["SUPABASE_API_TLS_CERT_PATH"]; + delete testEnv["SUPABASE_API_TLS_KEY_PATH"]; + }); + + it.live( + "prefers a remote-set api.tls.cert_path/key_path over a conflicting (missing-file) env override", + () => + Effect.gen(function* () { + // The env vars point at files that don't exist; the load only succeeds if the remote-set + // paths win instead. + yield* writeTlsFile(tempRoot.current, "cert.pem"); + yield* writeTlsFile(tempRoot.current, "key.pem"); + testEnv["SUPABASE_API_TLS_CERT_PATH"] = "missing-cert.pem"; + testEnv["SUPABASE_API_TLS_KEY_PATH"] = "missing-key.pem"; + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "key.pem" } }, + }); + yield* resolveLocalConfigValues( + config, + "127.0.0.1", + tempRoot.current, + undefined, + undefined, + new Set(["api.tls.cert_path", "api.tls.key_path"]), + ); + }).pipe(Effect.provide(testLayer)), + ); - it("still uses the env override when no remote block matched", () => { - writeTlsFile(tempRoot.current, "cert.pem"); - process.env["SUPABASE_API_TLS_CERT_PATH"] = "missing-cert.pem"; - const config = baseConfig({ - api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "cert.pem" } }, - }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current)).toThrow( - "failed to read TLS cert: ", - ); - }); + it.live("still uses the env override when no remote block matched", () => + Effect.gen(function* () { + yield* writeTlsFile(tempRoot.current, "cert.pem"); + testEnv["SUPABASE_API_TLS_CERT_PATH"] = "missing-cert.pem"; + const config = baseConfig({ + api: { tls: { enabled: true, cert_path: "cert.pem", key_path: "cert.pem" } }, + }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current))) + .message, + ).toContain("failed to read TLS cert: "); + }).pipe(Effect.provide(testLayer)), + ); }); it("prefers remote-set api.port/api.tls.enabled/api.external_url over conflicting env overrides", () => { - process.env["SUPABASE_API_PORT"] = "9999"; - process.env["SUPABASE_API_TLS_ENABLED"] = "true"; - process.env["SUPABASE_API_EXTERNAL_URL"] = "https://env-should-not-win.test"; + testEnv["SUPABASE_API_PORT"] = "9999"; + testEnv["SUPABASE_API_TLS_ENABLED"] = "true"; + testEnv["SUPABASE_API_EXTERNAL_URL"] = "https://env-should-not-win.test"; const config = baseConfig({ api: { port: 54321, external_url: "", tls: { enabled: false } } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["api.port", "api.tls.enabled", "api.external_url"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["api.port", "api.tls.enabled", "api.external_url"]), + ), ); expect(values.apiUrl).toBe("http://127.0.0.1:54321"); }); it("prefers a remote-set db.port over a conflicting SUPABASE_DB_PORT", () => { - process.env["SUPABASE_DB_PORT"] = "9999"; + testEnv["SUPABASE_DB_PORT"] = "9999"; const config = baseConfig({ db: { port: 54322 } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["db.port"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["db.port"]), + ), ); expect(values.dbPort).toBe(54322); expect(values.dbUrl).toContain(":54322/postgres"); }); it("prefers remote-set auth.site_url/auth.jwt_expiry over conflicting env overrides", () => { - process.env["SUPABASE_AUTH_SITE_URL"] = "https://env-should-not-win.test"; - process.env["SUPABASE_AUTH_JWT_EXPIRY"] = "9999"; + testEnv["SUPABASE_AUTH_SITE_URL"] = "https://env-should-not-win.test"; + testEnv["SUPABASE_AUTH_JWT_EXPIRY"] = "9999"; const config = baseConfig({ auth: { site_url: "https://remote.test", jwt_expiry: 3600 } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.site_url", "auth.jwt_expiry"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.site_url", "auth.jwt_expiry"]), + ), ); expect(values.authSiteUrl).toBe("https://remote.test"); expect(values.authJwtExpiry).toBe(3600); }); it("prefers remote-set auth.anon_key/auth.service_role_key over conflicting env overrides", () => { - process.env["SUPABASE_AUTH_ANON_KEY"] = "env-anon-key"; - process.env["SUPABASE_AUTH_SERVICE_ROLE_KEY"] = "env-service-role-key"; + testEnv["SUPABASE_AUTH_ANON_KEY"] = "env-anon-key"; + testEnv["SUPABASE_AUTH_SERVICE_ROLE_KEY"] = "env-service-role-key"; const config = baseConfig({ auth: { anon_key: "remote-anon-key", service_role_key: "remote-service-role-key" }, }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.anon_key", "auth.service_role_key"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.anon_key", "auth.service_role_key"]), + ), ); expect(values.anonKey).toBe("remote-anon-key"); expect(values.serviceRoleKey).toBe("remote-service-role-key"); }); it("suppresses a malformed SUPABASE_STUDIO_API_URL when a remote block already set studio.api_url", () => { - process.env["SUPABASE_STUDIO_API_URL"] = "http://[::1"; + testEnv["SUPABASE_STUDIO_API_URL"] = "http://[::1"; const config = baseConfig({ studio: { api_url: "http://remote.test" } }); expect(() => + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["studio.api_url"]), + ), + ), + ).not.toThrow(); + }); + + it("prefers a remote-set studio.openai_api_key over a conflicting SUPABASE_STUDIO_OPENAI_API_KEY", () => { + testEnv["SUPABASE_STUDIO_OPENAI_API_KEY"] = "encrypted:not-a-real-ciphertext"; + const config = baseConfig({ studio: { openai_api_key: "remote-openai-key" } }); + const values = run( resolveLocalConfigValues( config, "127.0.0.1", WORKDIR, undefined, undefined, - new Set(["studio.api_url"]), + new Set(["studio.openai_api_key"]), ), - ).not.toThrow(); - }); - - it("prefers a remote-set studio.openai_api_key over a conflicting SUPABASE_STUDIO_OPENAI_API_KEY", () => { - process.env["SUPABASE_STUDIO_OPENAI_API_KEY"] = "encrypted:not-a-real-ciphertext"; - const config = baseConfig({ studio: { openai_api_key: "remote-openai-key" } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["studio.openai_api_key"]), ); expect(values.openaiApiKey).toBe("remote-openai-key"); }); it("still rejects a malformed SUPABASE_STUDIO_OPENAI_API_KEY when no remote block matched", () => { - process.env["SUPABASE_STUDIO_OPENAI_API_KEY"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_STUDIO_OPENAI_API_KEY"] = "encrypted:not-a-real-ciphertext"; const config = baseConfig({ studio: { openai_api_key: "remote-openai-key" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "failed to parse config: missing private key", ); }); it("prefers remote-set auth.publishable_key/auth.secret_key over conflicting env overrides", () => { - process.env["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "encrypted:not-a-real-ciphertext"; - process.env["SUPABASE_AUTH_SECRET_KEY"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_AUTH_SECRET_KEY"] = "encrypted:not-a-real-ciphertext"; const config = baseConfig({ auth: { publishable_key: "remote-publishable-key", secret_key: "remote-secret-key" }, }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.publishable_key", "auth.secret_key"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.publishable_key", "auth.secret_key"]), + ), ); expect(values.publishableKey).toBe("remote-publishable-key"); expect(values.secretKey).toBe("remote-secret-key"); }); it("still rejects a malformed SUPABASE_AUTH_PUBLISHABLE_KEY when no remote block matched", () => { - process.env["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_AUTH_PUBLISHABLE_KEY"] = "encrypted:not-a-real-ciphertext"; const config = baseConfig({ auth: { publishable_key: "remote-publishable-key" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "failed to parse config: missing private key", ); }); it("still rejects a malformed SUPABASE_AUTH_SECRET_KEY when no remote block matched", () => { - process.env["SUPABASE_AUTH_SECRET_KEY"] = "encrypted:not-a-real-ciphertext"; + testEnv["SUPABASE_AUTH_SECRET_KEY"] = "encrypted:not-a-real-ciphertext"; const config = baseConfig({ auth: { secret_key: "remote-secret-key" } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "failed to parse config: missing private key", ); }); it("suppresses a malformed SUPABASE_DB_SETTINGS_MAX_CONNECTIONS when the remote block set db.settings.max_connections", () => { - process.env["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "not-a-number"; + testEnv["SUPABASE_DB_SETTINGS_MAX_CONNECTIONS"] = "not-a-number"; const config = baseConfig({ db: { settings: { max_connections: 100 } } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["db.settings.max_connections"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["db.settings.max_connections"]), + ), ), ).not.toThrow(); }); - it("suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", () => { - process.env["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.enabled"]), - ), - ).not.toThrow(); - }); + it.live( + "suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", + () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; + const config = baseConfig({ auth: { enabled: false } }); + yield* resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.enabled"]), + ); + }).pipe(Effect.provide(testLayer)), + ); - it("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", () => { - process.env["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( - 'Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool', - ); - }); + it.live("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; + const config = baseConfig({ auth: { enabled: false } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain('Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool'); + }).pipe(Effect.provide(testLayer)), + ); it("suppresses a malformed SUPABASE_ANALYTICS_ENABLED when a remote block already set analytics.enabled", () => { - process.env["SUPABASE_ANALYTICS_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_ANALYTICS_ENABLED"] = "not-a-bool"; const config = baseConfig({ analytics: { enabled: false } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["analytics.enabled"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["analytics.enabled"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_ANALYTICS_ENABLED when no remote block matched", () => { - process.env["SUPABASE_ANALYTICS_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_ANALYTICS_ENABLED"] = "not-a-bool"; const config = baseConfig({ analytics: { enabled: false } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'Invalid config for analytics.enabled: cannot parse "not-a-bool" as a bool', ); }); it("prefers a remote-set analytics.gcp_project_id over a conflicting SUPABASE_ANALYTICS_GCP_PROJECT_ID", () => { - process.env["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "env-project"; + testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "env-project"; const config = baseConfig({ analytics: { gcp_project_id: "remote-project" } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["analytics.gcp_project_id"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["analytics.gcp_project_id"]), + ), ); expect(values.gcpProjectId).toBe("remote-project"); - delete process.env["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; }); it("still applies SUPABASE_ANALYTICS_GCP_PROJECT_ID when no remote block matched", () => { - process.env["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "env-project"; + testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"] = "env-project"; const config = baseConfig({ analytics: { gcp_project_id: "remote-project" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.gcpProjectId).toBe("env-project"); - delete process.env["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_ID"]; }); it("prefers a remote-set analytics.gcp_project_number over a conflicting SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER", () => { - process.env["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "999"; + testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "999"; const config = baseConfig({ analytics: { gcp_project_number: "111" } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["analytics.gcp_project_number"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["analytics.gcp_project_number"]), + ), ); expect(values.gcpProjectNumber).toBe("111"); - delete process.env["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; }); it("still applies SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER when no remote block matched", () => { - process.env["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "999"; + testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"] = "999"; const config = baseConfig({ analytics: { gcp_project_number: "111" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.gcpProjectNumber).toBe("999"); - delete process.env["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER"]; }); it("prefers a remote-set analytics.gcp_jwt_path over a conflicting SUPABASE_ANALYTICS_GCP_JWT_PATH", () => { - process.env["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "env-key.json"; + testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "env-key.json"; const config = baseConfig({ analytics: { gcp_jwt_path: "remote-key.json" } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["analytics.gcp_jwt_path"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["analytics.gcp_jwt_path"]), + ), ); expect(values.gcpJwtPath).toBe("remote-key.json"); - delete process.env["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; }); it("still applies SUPABASE_ANALYTICS_GCP_JWT_PATH when no remote block matched", () => { - process.env["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "env-key.json"; + testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"] = "env-key.json"; const config = baseConfig({ analytics: { gcp_jwt_path: "remote-key.json" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.gcpJwtPath).toBe("env-key.json"); - delete process.env["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; + delete testEnv["SUPABASE_ANALYTICS_GCP_JWT_PATH"]; }); it("prefers a remote-set auth.jwt_issuer over a conflicting SUPABASE_AUTH_JWT_ISSUER", () => { - process.env["SUPABASE_AUTH_JWT_ISSUER"] = "https://env.example.com"; + testEnv["SUPABASE_AUTH_JWT_ISSUER"] = "https://env.example.com"; const config = baseConfig({ auth: { jwt_issuer: "https://remote.example.com" } }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.jwt_issuer"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.jwt_issuer"]), + ), ); expect(values.authJwtIssuer).toBe("https://remote.example.com"); - delete process.env["SUPABASE_AUTH_JWT_ISSUER"]; + delete testEnv["SUPABASE_AUTH_JWT_ISSUER"]; }); it("still applies SUPABASE_AUTH_JWT_ISSUER when no remote block matched", () => { - process.env["SUPABASE_AUTH_JWT_ISSUER"] = "https://env.example.com"; + testEnv["SUPABASE_AUTH_JWT_ISSUER"] = "https://env.example.com"; const config = baseConfig({ auth: { jwt_issuer: "https://remote.example.com" } }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.authJwtIssuer).toBe("https://env.example.com"); - delete process.env["SUPABASE_AUTH_JWT_ISSUER"]; + delete testEnv["SUPABASE_AUTH_JWT_ISSUER"]; }); it("prefers a remote-set auth.additional_redirect_urls over a conflicting SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS", () => { - process.env["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = "https://env.example.com"; + testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = "https://env.example.com"; const config = baseConfig({ auth: { additional_redirect_urls: ["https://remote.example.com"] }, }); - const values = resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.additional_redirect_urls"]), + const values = run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.additional_redirect_urls"]), + ), ); expect(values.authAdditionalRedirectUrls).toEqual(["https://remote.example.com"]); - delete process.env["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"]; + delete testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"]; }); it("still applies SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS when no remote block matched", () => { - process.env["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = "https://env.example.com"; + testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"] = "https://env.example.com"; const config = baseConfig({ auth: { additional_redirect_urls: ["https://remote.example.com"] }, }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const values = run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)); expect(values.authAdditionalRedirectUrls).toEqual(["https://env.example.com"]); - delete process.env["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"]; + delete testEnv["SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS"]; }); describe("auth.webauthn.rp_id / auth.webauthn.rp_origins — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { @@ -3855,259 +4123,281 @@ describe("resolveLocalConfigValues — remoteOverrideKeys (linked shadow provisi // validateResolvedConfig's own emptiness check on a document that leaves the field present // but empty. afterEach(() => { - delete process.env["SUPABASE_AUTH_PASSKEY_ENABLED"]; - delete process.env["SUPABASE_AUTH_WEBAUTHN_RP_ID"]; - delete process.env["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"]; + delete testEnv["SUPABASE_AUTH_PASSKEY_ENABLED"]; + delete testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"]; + delete testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"]; }); it("suppresses a non-empty SUPABASE_AUTH_WEBAUTHN_RP_ID when a remote block already set (empty) auth.webauthn.rp_id", () => { - process.env["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; + testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; const config = baseConfig(); const document = { auth: { passkey: { enabled: true }, webauthn: { rp_id: "", rp_origins: ["http://x"] } }, }; expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - document, - new Set(["auth.webauthn.rp_id"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + document, + new Set(["auth.webauthn.rp_id"]), + ), ), ).toThrow("Missing required field in config: auth.webauthn.rp_id"); }); it("still applies SUPABASE_AUTH_WEBAUTHN_RP_ID when no remote block matched", () => { - process.env["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; + testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ID"] = "localhost"; const config = baseConfig(); const document = { auth: { passkey: { enabled: true }, webauthn: { rp_id: "", rp_origins: ["http://x"] } }, }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); it("suppresses a non-empty SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS when a remote block already set (empty) auth.webauthn.rp_origins", () => { - process.env["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = "http://localhost:3000"; + testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = "http://localhost:3000"; const config = baseConfig(); const document = { auth: { passkey: { enabled: true }, webauthn: { rp_id: "localhost", rp_origins: [] } }, }; expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - document, - new Set(["auth.webauthn.rp_origins"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + document, + new Set(["auth.webauthn.rp_origins"]), + ), ), ).toThrow("Missing required field in config: auth.webauthn.rp_origins"); }); it("still applies SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS when no remote block matched", () => { - process.env["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = "http://localhost:3000"; + testEnv["SUPABASE_AUTH_WEBAUTHN_RP_ORIGINS"] = "http://localhost:3000"; const config = baseConfig(); const document = { auth: { passkey: { enabled: true }, webauthn: { rp_id: "localhost", rp_origins: [] } }, }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).not.toThrow(); }); }); it("suppresses a malformed SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED when a remote block already set auth.third_party.firebase.enabled", () => { - process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "not-a-bool"; const config = baseConfig({ auth: { enabled: true, third_party: { firebase: { enabled: false } } }, }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.enabled", "auth.third_party.firebase.enabled"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.enabled", "auth.third_party.firebase.enabled"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED when no remote block matched", () => { - process.env["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED"] = "not-a-bool"; const config = baseConfig({ auth: { enabled: true, third_party: { firebase: { enabled: false } } }, }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'Invalid config for auth.third_party.firebase.enabled: cannot parse "not-a-bool" as a bool', ); }); it("suppresses a malformed SUPABASE_EDGE_RUNTIME_DENO_VERSION when a remote block already set edge_runtime.deno_version", () => { - process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; + testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; const config = baseConfig({ edge_runtime: { deno_version: 2 } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["edge_runtime.deno_version"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["edge_runtime.deno_version"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_EDGE_RUNTIME_DENO_VERSION when no remote block matched", () => { - process.env["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; + testEnv["SUPABASE_EDGE_RUNTIME_DENO_VERSION"] = "abc"; const config = baseConfig({ edge_runtime: { deno_version: 2 } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( "Failed reading config: Invalid edge_runtime.deno_version: abc.", ); }); it("suppresses a malformed SUPABASE_API_ENABLED when a remote block already set api.enabled", () => { - process.env["SUPABASE_API_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_API_ENABLED"] = "not-a-bool"; const config = baseConfig({ api: { enabled: false } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["api.enabled"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["api.enabled"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_API_ENABLED when no remote block matched", () => { - process.env["SUPABASE_API_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_API_ENABLED"] = "not-a-bool"; const config = baseConfig({ api: { enabled: false } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'Invalid config for api.enabled: cannot parse "not-a-bool" as a bool', ); }); it("suppresses a malformed SUPABASE_STUDIO_ENABLED when a remote block already set studio.enabled", () => { - process.env["SUPABASE_STUDIO_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_STUDIO_ENABLED"] = "not-a-bool"; const config = baseConfig({ studio: { enabled: false } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["studio.enabled"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["studio.enabled"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_STUDIO_ENABLED when no remote block matched", () => { - process.env["SUPABASE_STUDIO_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_STUDIO_ENABLED"] = "not-a-bool"; const config = baseConfig({ studio: { enabled: false } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'Invalid config for studio.enabled: cannot parse "not-a-bool" as a bool', ); }); it("suppresses a malformed SUPABASE_STUDIO_PORT when a remote block already set studio.port", () => { - process.env["SUPABASE_STUDIO_PORT"] = "not-a-port"; + testEnv["SUPABASE_STUDIO_PORT"] = "not-a-port"; const config = baseConfig({ studio: { port: 54323 } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["studio.port"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["studio.port"]), + ), ), ).not.toThrow(); }); it("suppresses a malformed SUPABASE_LOCAL_SMTP_ENABLED when a remote block already set local_smtp.enabled", () => { - process.env["SUPABASE_LOCAL_SMTP_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_LOCAL_SMTP_ENABLED"] = "not-a-bool"; const config = baseConfig({ local_smtp: { enabled: false } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["local_smtp.enabled"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["local_smtp.enabled"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_LOCAL_SMTP_ENABLED when no remote block matched", () => { - process.env["SUPABASE_LOCAL_SMTP_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_LOCAL_SMTP_ENABLED"] = "not-a-bool"; const config = baseConfig({ local_smtp: { enabled: false } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'Invalid config for local_smtp.enabled: cannot parse "not-a-bool" as a bool', ); }); it("suppresses a malformed SUPABASE_AUTH_ENABLE_SIGNUP when a remote block already set auth.enable_signup", () => { - process.env["SUPABASE_AUTH_ENABLE_SIGNUP"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_ENABLE_SIGNUP"] = "not-a-bool"; const config = baseConfig({ auth: { enable_signup: false } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.enable_signup"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.enable_signup"]), + ), ), ).not.toThrow(); }); it("still rejects a malformed SUPABASE_AUTH_ENABLE_SIGNUP when no remote block matched", () => { - process.env["SUPABASE_AUTH_ENABLE_SIGNUP"] = "not-a-bool"; + testEnv["SUPABASE_AUTH_ENABLE_SIGNUP"] = "not-a-bool"; const config = baseConfig({ auth: { enable_signup: false } }); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toThrow( + expect(() => run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).toThrow( 'Invalid config for auth.enable_signup: cannot parse "not-a-bool" as a bool', ); }); it("suppresses a malformed SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH when a remote block already set auth.minimum_password_length", () => { - process.env["SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH"] = "not-a-number"; + testEnv["SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH"] = "not-a-number"; const config = baseConfig({ auth: { minimum_password_length: 8 } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["auth.minimum_password_length"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["auth.minimum_password_length"]), + ), ), ).not.toThrow(); }); it("suppresses a malformed SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED when a remote block already set experimental.webhooks.enabled", () => { - process.env["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "not-a-bool"; + testEnv["SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED"] = "not-a-bool"; const config = baseConfig({ experimental: { webhooks: { enabled: true } } }); expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - undefined, - new Set(["experimental.webhooks.enabled"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + undefined, + new Set(["experimental.webhooks.enabled"]), + ), ), ).not.toThrow(); }); it("suppresses a scheme-invalid SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI when a remote block already set that hook's uri", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; const config = baseConfig({ auth: { hook: { @@ -4121,19 +4411,21 @@ describe("resolveLocalConfigValues — remoteOverrideKeys (linked shadow provisi }); const document = { auth: { hook: { custom_access_token: { enabled: true } } } }; expect(() => - resolveLocalConfigValues( - config, - "127.0.0.1", - WORKDIR, - undefined, - document, - new Set(["auth.hook.custom_access_token.uri"]), + run( + resolveLocalConfigValues( + config, + "127.0.0.1", + WORKDIR, + undefined, + document, + new Set(["auth.hook.custom_access_token.uri"]), + ), ), ).not.toThrow(); }); it("still rejects a scheme-invalid SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI when no remote block matched that leaf", () => { - process.env["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; + testEnv["SUPABASE_AUTH_HOOK_CUSTOM_ACCESS_TOKEN_URI"] = "ftp://example.com"; const config = baseConfig({ auth: { hook: { @@ -4143,7 +4435,7 @@ describe("resolveLocalConfigValues — remoteOverrideKeys (linked shadow provisi }); const document = { auth: { hook: { custom_access_token: { enabled: true } } } }; expect(() => - resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document), + run(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, undefined, document)), ).toThrow("auth.hook.custom_access_token.uri should be a HTTP, HTTPS, or pg-functions URI"); }); }); @@ -4151,218 +4443,276 @@ describe("resolveLocalConfigValues — remoteOverrideKeys (linked shadow provisi describe("resolveLocalJwks", () => { const tempRoot = useTempWorkdir("supabase-local-jwks-test-"); - it("includes the default ES256 signing key and the oct JWT-secret fallback when no signing_keys_path is configured", async () => { - const config = baseConfig(); - const jwks = await runLocalJwks(config, tempRoot.current, "a".repeat(32)); - expect(JSON.parse(jwks)).toEqual({ - keys: [ - { - kty: "EC", - kid: "b81269f1-21d8-4f2e-b719-c2240a840d90", - use: "sig", - key_ops: ["verify"], - alg: "ES256", - ext: true, - crv: "P-256", - x: "M5Sjqn5zwC9Kl1zVfUUGvv9boQjCGd45G8sdopBExB4", - y: "P6IXMvA2WYXSHSOMTBH2jsw_9rrzGy89FjPf6oOsIxQ", - }, - { kty: "oct", k: Buffer.from("a".repeat(32)).toString("base64url") }, - ], - }); - }); + it.live( + "includes the default ES256 signing key and the oct JWT-secret fallback when no signing_keys_path is configured", + () => + Effect.gen(function* () { + const config = baseConfig(); + const jwks = yield* runLocalJwks(config, tempRoot.current, "a".repeat(32)); + expect(yield* decodeJson(jwks)).toEqual({ + keys: [ + { + kty: "EC", + kid: "b81269f1-21d8-4f2e-b719-c2240a840d90", + use: "sig", + key_ops: ["verify"], + alg: "ES256", + ext: true, + crv: "P-256", + x: "M5Sjqn5zwC9Kl1zVfUUGvv9boQjCGd45G8sdopBExB4", + y: "P6IXMvA2WYXSHSOMTBH2jsw_9rrzGy89FjPf6oOsIxQ", + }, + { kty: "oct", k: Buffer.from("a".repeat(32)).toString("base64url") }, + ], + }); + }).pipe(Effect.provide(testLayer)), + ); - it("publishes the public form of every signing key and omits the oct fallback", async () => { - const jwk = generateRsaJwk(); - writeSigningKeys(tempRoot.current, [jwk]); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const jwks = await runLocalJwks(config, tempRoot.current, "a".repeat(32)); - const parsed = JSON.parse(jwks) as { keys: ReadonlyArray> }; - - expect(parsed.keys).toHaveLength(1); - expect(parsed.keys[0]).toMatchObject({ - kty: "RSA", - kid: "test-rsa-kid", - n: jwk["n"], - e: jwk["e"], - }); - expect(parsed.keys[0]).not.toHaveProperty("d"); - expect(parsed.keys[0]).not.toHaveProperty("p"); - expect(parsed.keys.some((key) => key["kty"] === "oct")).toBe(false); - }); + it.live("publishes the public form of every signing key and omits the oct fallback", () => + Effect.gen(function* () { + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const jwks = yield* runLocalJwks(config, tempRoot.current, "a".repeat(32)); + const parsed = (yield* decodeJson(jwks)) as { keys: ReadonlyArray> }; - it("preserves a configured signing key's use/ext and filters key_ops to verify-only", async () => { - const jwk = { ...generateRsaJwk(), use: "sig", ext: true, key_ops: ["sign", "verify"] }; - writeSigningKeys(tempRoot.current, [jwk]); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const jwks = await runLocalJwks(config, tempRoot.current, "a".repeat(32)); - const parsed = JSON.parse(jwks) as { keys: ReadonlyArray> }; + expect(parsed.keys).toHaveLength(1); + expect(parsed.keys[0]).toMatchObject({ + kty: "RSA", + kid: "test-rsa-kid", + n: jwk["n"], + e: jwk["e"], + }); + expect(parsed.keys[0]).not.toHaveProperty("d"); + expect(parsed.keys[0]).not.toHaveProperty("p"); + expect(parsed.keys.some((key) => key["kty"] === "oct")).toBe(false); + }).pipe(Effect.provide(testLayer)), + ); - expect(parsed.keys[0]).toMatchObject({ use: "sig", ext: true, key_ops: ["verify"] }); - }); + it.live("preserves a configured signing key's use/ext and filters key_ops to verify-only", () => + Effect.gen(function* () { + const jwk = { ...generateRsaJwk(), use: "sig", ext: true, key_ops: ["sign", "verify"] }; + yield* writeSigningKeys(tempRoot.current, [jwk]); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const jwks = yield* runLocalJwks(config, tempRoot.current, "a".repeat(32)); + const parsed = (yield* decodeJson(jwks)) as { keys: ReadonlyArray> }; - it("falls back to the default ES256 signing key (not the configured file, not the oct fallback) when auth is disabled but signing_keys_path is set", async () => { - writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - const config = baseConfig({ - auth: { enabled: false, signing_keys_path: "signing_keys.json" }, - }); - const jwks = await runLocalJwks(config, tempRoot.current, "a".repeat(32)); - expect(JSON.parse(jwks)).toEqual({ - keys: [ - { - kty: "EC", - kid: "b81269f1-21d8-4f2e-b719-c2240a840d90", - use: "sig", - key_ops: ["verify"], - alg: "ES256", - ext: true, - crv: "P-256", - x: "M5Sjqn5zwC9Kl1zVfUUGvv9boQjCGd45G8sdopBExB4", - y: "P6IXMvA2WYXSHSOMTBH2jsw_9rrzGy89FjPf6oOsIxQ", - }, - ], - }); - }); + expect(parsed.keys[0]).toMatchObject({ use: "sig", ext: true, key_ops: ["verify"] }); + }).pipe(Effect.provide(testLayer)), + ); - it("throws a Go-worded error when the signing keys file does not exist", async () => { - const config = baseConfig({ auth: { signing_keys_path: "missing.json" } }); - await expect(runLocalJwks(config, tempRoot.current, "a".repeat(32))).rejects.toThrow( - "failed to read signing keys: ", - ); - }); + it.live( + "falls back to the default ES256 signing key (not the configured file, not the oct fallback) when auth is disabled but signing_keys_path is set", + () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "signing_keys.json" }, + }); + const jwks = yield* runLocalJwks(config, tempRoot.current, "a".repeat(32)); + expect(yield* decodeJson(jwks)).toEqual({ + keys: [ + { + kty: "EC", + kid: "b81269f1-21d8-4f2e-b719-c2240a840d90", + use: "sig", + key_ops: ["verify"], + alg: "ES256", + ext: true, + crv: "P-256", + x: "M5Sjqn5zwC9Kl1zVfUUGvv9boQjCGd45G8sdopBExB4", + y: "P6IXMvA2WYXSHSOMTBH2jsw_9rrzGy89FjPf6oOsIxQ", + }, + ], + }); + }).pipe(Effect.provide(testLayer)), + ); - it("throws a Go-worded error when the signing keys file is malformed JSON", async () => { - const supabaseDir = join(tempRoot.current, "supabase"); - mkdirSync(supabaseDir, { recursive: true }); - writeFileSync(join(supabaseDir, "signing_keys.json"), "not valid json"); - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - await expect(runLocalJwks(config, tempRoot.current, "a".repeat(32))).rejects.toThrow( - "failed to decode signing keys: ", - ); - }); + it.live("throws a Go-worded error when the signing keys file does not exist", () => + Effect.gen(function* () { + const config = baseConfig({ auth: { signing_keys_path: "missing.json" } }); + expect( + (yield* Effect.flip(runLocalJwks(config, tempRoot.current, "a".repeat(32)))).message, + ).toContain("failed to read signing keys: "); + }).pipe(Effect.provide(testLayer)), + ); + + it.live("throws a Go-worded error when the signing keys file is malformed JSON", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const supabaseDir = path.join(tempRoot.current, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString(path.join(supabaseDir, "signing_keys.json"), "not valid json"); + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(runLocalJwks(config, tempRoot.current, "a".repeat(32)))).message, + ).toContain("failed to decode signing keys: "); + }).pipe(Effect.provide(testLayer)), + ); describe("auth.third_party", () => { afterEach(() => { vi.restoreAllMocks(); }); - it("rejects an enabled third-party provider missing its required field", async () => { - const config = baseConfig({ auth: { third_party: { firebase: { enabled: true } } } }); - await expect(runLocalJwks(config, WORKDIR, "a".repeat(32))).rejects.toThrow( - "Invalid config: auth.third_party.firebase is enabled but without a project_id.", - ); - }); + it.live("rejects an enabled third-party provider missing its required field", () => + Effect.gen(function* () { + const config = baseConfig({ auth: { third_party: { firebase: { enabled: true } } } }); + expect( + (yield* Effect.flip(runLocalJwks(config, WORKDIR, "a".repeat(32)))).message, + ).toContain( + "Invalid config: auth.third_party.firebase is enabled but without a project_id.", + ); + }).pipe(Effect.provide(testLayer)), + ); - it("rejects more than one enabled third-party provider", async () => { - const config = baseConfig({ - auth: { - third_party: { - firebase: { enabled: true, project_id: "my-project" }, - workos: { enabled: true, issuer_url: "https://issuer.example" }, + it.live("rejects more than one enabled third-party provider", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + third_party: { + firebase: { enabled: true, project_id: "my-project" }, + workos: { enabled: true, issuer_url: "https://issuer.example" }, + }, }, - }, - }); - await expect(runLocalJwks(config, WORKDIR, "a".repeat(32))).rejects.toThrow( - "Invalid config: Only one third_party provider allowed to be enabled at a time.", - ); - }); + }); + expect( + (yield* Effect.flip(runLocalJwks(config, WORKDIR, "a".repeat(32)))).message, + ).toContain( + "Invalid config: Only one third_party provider allowed to be enabled at a time.", + ); + }).pipe(Effect.provide(testLayer)), + ); - it("does not validate third-party providers when auth is disabled, matching Go's ResolveJWKS/IssuerURL", async () => { - const remoteKeys = [{ kty: "RSA", kid: "firebase-key", n: "abc", e: "AQAB" }]; - const issuerUrl = "https://securetoken.google.com/my-project"; - const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { - const url = - typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - if (url === `${issuerUrl}/.well-known/openid-configuration`) { - return new Response(JSON.stringify({ jwks_uri: `${issuerUrl}/jwks.json` }), { - status: 200, - headers: { "content-type": "application/json" }, + it.live( + "does not validate third-party providers when auth is disabled, matching Go's ResolveJWKS/IssuerURL", + () => + Effect.gen(function* () { + const remoteKeys = [{ kty: "RSA", kid: "firebase-key", n: "abc", e: "AQAB" }]; + const issuerUrl = "https://securetoken.google.com/my-project"; + const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation((input) => { + const url = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; + if (url === `${issuerUrl}/.well-known/openid-configuration`) { + return Promise.resolve( + new Response(JSON.stringify({ jwks_uri: `${issuerUrl}/jwks.json` }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + } + if (url === `${issuerUrl}/jwks.json`) { + return Promise.resolve( + new Response(JSON.stringify({ keys: remoteKeys }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + } + return Promise.reject(new Error(`unexpected fetch: ${url}`)); }); - } - if (url === `${issuerUrl}/jwks.json`) { - return new Response(JSON.stringify({ keys: remoteKeys }), { - status: 200, - headers: { "content-type": "application/json" }, + const config = baseConfig({ + auth: { + enabled: false, + third_party: { + firebase: { enabled: true, project_id: "my-project" }, + workos: { enabled: true, issuer_url: "https://issuer.example" }, + }, + }, }); - } - throw new Error(`unexpected fetch: ${url}`); - }); - const config = baseConfig({ - auth: { - enabled: false, - third_party: { - firebase: { enabled: true, project_id: "my-project" }, - workos: { enabled: true, issuer_url: "https://issuer.example" }, - }, - }, - }); - const jwksJson = await runLocalJwks(config, WORKDIR, "a".repeat(32)); - const jwks = JSON.parse(jwksJson) as { keys: ReadonlyArray<{ kid?: string }> }; - expect(jwks.keys.some((key) => key.kid === "firebase-key")).toBe(true); - fetchMock.mockRestore(); - }); + const jwksJson = yield* runLocalJwks(config, WORKDIR, "a".repeat(32)); + const jwks = (yield* decodeJson(jwksJson)) as { keys: ReadonlyArray<{ kid?: string }> }; + expect(jwks.keys.some((key) => key.kid === "firebase-key")).toBe(true); + fetchMock.mockRestore(); + }).pipe(Effect.provide(testLayer)), + ); - it('does not attempt a remote JWKS fetch for an enabled third-party provider with an empty issuer_url, matching Go\'s issuerURL != "" check', async () => { - const fetchMock = vi.spyOn(globalThis, "fetch"); - const config = baseConfig({ - auth: { - enabled: false, - third_party: { workos: { enabled: true, issuer_url: "" } }, - }, - }); + it.live( + 'does not attempt a remote JWKS fetch for an enabled third-party provider with an empty issuer_url, matching Go\'s issuerURL != "" check', + () => + Effect.gen(function* () { + const fetchMock = vi.spyOn(globalThis, "fetch"); + const config = baseConfig({ + auth: { + enabled: false, + third_party: { workos: { enabled: true, issuer_url: "" } }, + }, + }); - const jwksJson = await runLocalJwks(config, WORKDIR, "a".repeat(32)); - const jwks = JSON.parse(jwksJson) as { keys: ReadonlyArray }; + const jwksJson = yield* runLocalJwks(config, WORKDIR, "a".repeat(32)); + const jwks = (yield* decodeJson(jwksJson)) as { keys: ReadonlyArray }; - expect(fetchMock).not.toHaveBeenCalled(); - expect(jwks.keys.length).toBeGreaterThan(0); - fetchMock.mockRestore(); - }); + expect(fetchMock).not.toHaveBeenCalled(); + expect(jwks.keys.length).toBeGreaterThan(0); + fetchMock.mockRestore(); + }).pipe(Effect.provide(testLayer)), + ); - it("fetches and includes the remote JWKS for an enabled third-party provider", async () => { - const remoteKeys = [{ kty: "RSA", kid: "remote-key", n: "abc", e: "AQAB" }]; - const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { - const url = - typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - if (url === "https://issuer.example/.well-known/openid-configuration") { - return new Response(JSON.stringify({ jwks_uri: "https://issuer.example/jwks.json" }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - if (url === "https://issuer.example/jwks.json") { - return new Response(JSON.stringify({ keys: remoteKeys }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - throw new Error(`unexpected fetch url: ${url}`); - }); + it.live("fetches and includes the remote JWKS for an enabled third-party provider", () => + Effect.gen(function* () { + const remoteKeys = [{ kty: "RSA", kid: "remote-key", n: "abc", e: "AQAB" }]; + const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation((input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url === "https://issuer.example/.well-known/openid-configuration") { + return Promise.resolve( + new Response(JSON.stringify({ jwks_uri: "https://issuer.example/jwks.json" }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + } + if (url === "https://issuer.example/jwks.json") { + return Promise.resolve( + new Response(JSON.stringify({ keys: remoteKeys }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + } + return Promise.reject(new Error(`unexpected fetch url: ${url}`)); + }); - const config = baseConfig({ - auth: { third_party: { workos: { enabled: true, issuer_url: "https://issuer.example" } } }, - }); - const jwks = await runLocalJwks(config, WORKDIR, "a".repeat(32)); - const parsed = JSON.parse(jwks) as { keys: ReadonlyArray> }; + const config = baseConfig({ + auth: { + third_party: { workos: { enabled: true, issuer_url: "https://issuer.example" } }, + }, + }); + const jwks = yield* runLocalJwks(config, WORKDIR, "a".repeat(32)); + const parsed = (yield* decodeJson(jwks)) as { + keys: ReadonlyArray>; + }; - expect(parsed.keys).toEqual( - expect.arrayContaining([expect.objectContaining({ kid: "remote-key" })]), - ); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); + expect(parsed.keys).toEqual( + expect.arrayContaining([expect.objectContaining({ kid: "remote-key" })]), + ); + expect(fetchMock).toHaveBeenCalledTimes(2); + }).pipe(Effect.provide(testLayer)), + ); - it("fails the whole resolution when the remote JWKS fetch fails, unlike functions serve's leniency", async () => { - vi.spyOn(globalThis, "fetch").mockImplementation(async () => { - throw new Error("oidc discovery failed"); - }); + it.live( + "fails the whole resolution when the remote JWKS fetch fails, unlike functions serve's leniency", + () => + Effect.gen(function* () { + vi.spyOn(globalThis, "fetch").mockImplementation(() => + Promise.reject(new Error("oidc discovery failed")), + ); - const config = baseConfig({ - auth: { third_party: { workos: { enabled: true, issuer_url: "https://issuer.example" } } }, - }); - await expect(runLocalJwks(config, WORKDIR, "a".repeat(32))).rejects.toThrow( - "Failed to fetch https://issuer.example/.well-known/openid-configuration", - ); - }); + const config = baseConfig({ + auth: { + third_party: { workos: { enabled: true, issuer_url: "https://issuer.example" } }, + }, + }); + expect( + (yield* Effect.flip(runLocalJwks(config, WORKDIR, "a".repeat(32)))).message, + ).toContain("Failed to fetch https://issuer.example/.well-known/openid-configuration"); + }).pipe(Effect.provide(testLayer)), + ); }); describe("remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { @@ -4373,109 +4723,138 @@ describe("resolveLocalJwks", () => { "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", "SUPABASE_AUTH_ENABLED", ]) { - delete process.env[name]; + delete testEnv[name]; } }); - it("prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", async () => { - writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const jwks = await runLocalJwks( - config, - tempRoot.current, - "a".repeat(32), - undefined, - new Set(["auth.signing_keys_path"]), - ); - const parsed = JSON.parse(jwks) as { keys: ReadonlyArray> }; - expect(parsed.keys).toHaveLength(1); - expect(parsed.keys[0]).toMatchObject({ kty: "RSA", kid: "test-rsa-kid" }); - }); + it.live( + "prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", + () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); + testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const jwks = yield* runLocalJwks( + config, + tempRoot.current, + "a".repeat(32), + undefined, + new Set(["auth.signing_keys_path"]), + ); + const parsed = (yield* decodeJson(jwks)) as { + keys: ReadonlyArray>; + }; + expect(parsed.keys).toHaveLength(1); + expect(parsed.keys[0]).toMatchObject({ kty: "RSA", kid: "test-rsa-kid" }); + }).pipe(Effect.provide(testLayer)), + ); - it("still rejects a missing SUPABASE_AUTH_SIGNING_KEYS_PATH override when no remote block matched", async () => { - writeSigningKeys(tempRoot.current, [generateRsaJwk()]); - process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - await expect(runLocalJwks(config, tempRoot.current, "a".repeat(32))).rejects.toThrow( - "failed to read signing keys: ", - ); - }); + it.live( + "still rejects a missing SUPABASE_AUTH_SIGNING_KEYS_PATH override when no remote block matched", + () => + Effect.gen(function* () { + yield* writeSigningKeys(tempRoot.current, [generateRsaJwk()]); + testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(runLocalJwks(config, tempRoot.current, "a".repeat(32)))).message, + ).toContain("failed to read signing keys: "); + }).pipe(Effect.provide(testLayer)), + ); - it("prefers a remote-set auth.third_party.workos.* over conflicting env overrides", async () => { - const remoteKeys = [{ kty: "RSA", kid: "remote-key", n: "abc", e: "AQAB" }]; - const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => { - const url = - typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; - if (url === "https://remote-issuer.example/.well-known/openid-configuration") { - return new Response( - JSON.stringify({ jwks_uri: "https://remote-issuer.example/jwks.json" }), - { status: 200, headers: { "content-type": "application/json" } }, - ); - } - if (url === "https://remote-issuer.example/jwks.json") { - return new Response(JSON.stringify({ keys: remoteKeys }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - throw new Error(`unexpected fetch url: ${url}`); - }); - process.env["SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED"] = "false"; - process.env["SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL"] = - "https://env-should-not-win.test"; - const config = baseConfig({ - auth: { - third_party: { workos: { enabled: true, issuer_url: "https://remote-issuer.example" } }, - }, - }); - const jwks = await runLocalJwks( - config, - WORKDIR, - "a".repeat(32), - undefined, - new Set(["auth.third_party.workos.enabled", "auth.third_party.workos.issuer_url"]), - ); - const parsed = JSON.parse(jwks) as { keys: ReadonlyArray> }; - expect(parsed.keys.some((key) => key["kid"] === "remote-key")).toBe(true); - fetchMock.mockRestore(); - }); + it.live("prefers a remote-set auth.third_party.workos.* over conflicting env overrides", () => + Effect.gen(function* () { + const remoteKeys = [{ kty: "RSA", kid: "remote-key", n: "abc", e: "AQAB" }]; + const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation((input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url === "https://remote-issuer.example/.well-known/openid-configuration") { + return Promise.resolve( + new Response( + JSON.stringify({ jwks_uri: "https://remote-issuer.example/jwks.json" }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + } + if (url === "https://remote-issuer.example/jwks.json") { + return Promise.resolve( + new Response(JSON.stringify({ keys: remoteKeys }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + } + return Promise.reject(new Error(`unexpected fetch url: ${url}`)); + }); + testEnv["SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED"] = "false"; + testEnv["SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL"] = "https://env-should-not-win.test"; + const config = baseConfig({ + auth: { + third_party: { workos: { enabled: true, issuer_url: "https://remote-issuer.example" } }, + }, + }); + const jwks = yield* runLocalJwks( + config, + WORKDIR, + "a".repeat(32), + undefined, + new Set(["auth.third_party.workos.enabled", "auth.third_party.workos.issuer_url"]), + ); + const parsed = (yield* decodeJson(jwks)) as { + keys: ReadonlyArray>; + }; + expect(parsed.keys.some((key) => key["kid"] === "remote-key")).toBe(true); + fetchMock.mockRestore(); + }).pipe(Effect.provide(testLayer)), + ); - it("suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", async () => { - process.env["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - await expect( - runLocalJwks(config, WORKDIR, "a".repeat(32), undefined, new Set(["auth.enabled"])), - ).resolves.toEqual(expect.any(String)); - }); + it.live( + "suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", + () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; + const config = baseConfig({ auth: { enabled: false } }); + expect( + yield* runLocalJwks( + config, + WORKDIR, + "a".repeat(32), + undefined, + new Set(["auth.enabled"]), + ), + ).toEqual(expect.any(String)); + }).pipe(Effect.provide(testLayer)), + ); - it("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", async () => { - process.env["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - await expect(runLocalJwks(config, WORKDIR, "a".repeat(32))).rejects.toThrow( - 'Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool', - ); - }); + it.live("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; + const config = baseConfig({ auth: { enabled: false } }); + expect( + (yield* Effect.flip(runLocalJwks(config, WORKDIR, "a".repeat(32)))).message, + ).toContain('Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool'); + }).pipe(Effect.provide(testLayer)), + ); }); }); describe("resolveAuthExternalUrl — remoteOverrideKeys (linked shadow provisioning, CLI-1956)", () => { afterEach(() => { - delete process.env["SUPABASE_AUTH_EXTERNAL_URL"]; + delete testEnv["SUPABASE_AUTH_EXTERNAL_URL"]; }); it("prefers a remote-set auth.external_url over a conflicting SUPABASE_AUTH_EXTERNAL_URL", () => { - process.env["SUPABASE_AUTH_EXTERNAL_URL"] = "https://env-should-not-win.test"; + testEnv["SUPABASE_AUTH_EXTERNAL_URL"] = "https://env-should-not-win.test"; const document = { auth: { external_url: "https://remote.test" } }; - expect(resolveAuthExternalUrl(document, undefined, new Set(["auth.external_url"]))).toBe( + expect(run(resolveAuthExternalUrl(document, undefined, new Set(["auth.external_url"])))).toBe( "https://remote.test", ); }); it("still applies SUPABASE_AUTH_EXTERNAL_URL when no remote block matched", () => { - process.env["SUPABASE_AUTH_EXTERNAL_URL"] = "https://env-wins.test"; + testEnv["SUPABASE_AUTH_EXTERNAL_URL"] = "https://env-wins.test"; const document = { auth: { external_url: "https://configured.test" } }; - expect(resolveAuthExternalUrl(document, undefined)).toBe("https://env-wins.test"); + expect(run(resolveAuthExternalUrl(document, undefined))).toBe("https://env-wins.test"); }); }); @@ -4483,48 +4862,65 @@ describe("resolveConfiguredSigningKeys — remoteOverrideKeys (linked shadow pro const tempRoot = useTempWorkdir("supabase-configured-signing-keys-test-"); afterEach(() => { - delete process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"]; - delete process.env["SUPABASE_AUTH_ENABLED"]; + delete testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"]; + delete testEnv["SUPABASE_AUTH_ENABLED"]; }); - it("prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", () => { - const jwk = generateRsaJwk(); - writeSigningKeys(tempRoot.current, [jwk]); - process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - const keys = resolveConfiguredSigningKeys( - config, - tempRoot.current, - undefined, - new Set(["auth.signing_keys_path"]), - ); - expect(keys).toHaveLength(1); - expect(keys?.[0]).toMatchObject({ kid: "test-rsa-kid" }); - }); + it.live( + "prefers a remote-set auth.signing_keys_path over a conflicting SUPABASE_AUTH_SIGNING_KEYS_PATH", + () => + Effect.gen(function* () { + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + const keys = yield* resolveConfiguredSigningKeys( + config, + tempRoot.current, + undefined, + new Set(["auth.signing_keys_path"]), + ); + expect(keys).toHaveLength(1); + expect(keys?.[0]).toMatchObject({ kid: "test-rsa-kid" }); + }).pipe(Effect.provide(testLayer)), + ); - it("still reads the env-overridden path when no remote block matched", () => { - const jwk = generateRsaJwk(); - writeSigningKeys(tempRoot.current, [jwk]); - process.env["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; - const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); - expect(() => resolveConfiguredSigningKeys(config, tempRoot.current, undefined)).toThrow( - "failed to read signing keys: ", - ); - }); + it.live("still reads the env-overridden path when no remote block matched", () => + Effect.gen(function* () { + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + testEnv["SUPABASE_AUTH_SIGNING_KEYS_PATH"] = "missing-file.json"; + const config = baseConfig({ auth: { signing_keys_path: "signing_keys.json" } }); + expect( + (yield* Effect.flip(resolveConfiguredSigningKeys(config, tempRoot.current, undefined))) + .message, + ).toContain("failed to read signing keys: "); + }).pipe(Effect.provide(testLayer)), + ); - it("suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", () => { - process.env["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - expect(() => - resolveConfiguredSigningKeys(config, tempRoot.current, undefined, new Set(["auth.enabled"])), - ).not.toThrow(); - }); + it.live( + "suppresses a malformed SUPABASE_AUTH_ENABLED when a remote block already set auth.enabled", + () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; + const config = baseConfig({ auth: { enabled: false } }); + yield* resolveConfiguredSigningKeys( + config, + tempRoot.current, + undefined, + new Set(["auth.enabled"]), + ); + }).pipe(Effect.provide(testLayer)), + ); - it("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", () => { - process.env["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; - const config = baseConfig({ auth: { enabled: false } }); - expect(() => resolveConfiguredSigningKeys(config, tempRoot.current, undefined)).toThrow( - 'Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool', - ); - }); + it.live("still rejects a malformed SUPABASE_AUTH_ENABLED when no remote block matched", () => + Effect.gen(function* () { + testEnv["SUPABASE_AUTH_ENABLED"] = "not-a-bool"; + const config = baseConfig({ auth: { enabled: false } }); + expect( + (yield* Effect.flip(resolveConfiguredSigningKeys(config, tempRoot.current, undefined))) + .message, + ).toContain('Invalid config for auth.enabled: cannot parse "not-a-bool" as a bool'); + }).pipe(Effect.provide(testLayer)), + ); }); diff --git a/apps/cli/src/command-internal/local-project-context.ts b/apps/cli/src/command-internal/local-project-context.ts index b223e10253..6b910eeb3a 100644 --- a/apps/cli/src/command-internal/local-project-context.ts +++ b/apps/cli/src/command-internal/local-project-context.ts @@ -158,7 +158,7 @@ export const loadLocalProjectContext = ( /** Records OrioleDB selection for commands whose only local config read is this context. */ export const recordLocalProjectOrioleDbTelemetry = (context: LocalProjectContext) => - Effect.try(() => ({ + Effect.all({ version: envOverrideOrioleDBVersion( context.config.db.orioledb_version, context.projectEnvValues, @@ -167,7 +167,7 @@ export const recordLocalProjectOrioleDbTelemetry = (context: LocalProjectContext context.config.db.major_version, context.projectEnvValues, ), - })).pipe( + }).pipe( Effect.flatMap(({ version, majorVersion }) => recordOrioleDbTelemetry(version, majorVersion)), Effect.ignore, ); diff --git a/apps/cli/src/command-internal/path-containment.ts b/apps/cli/src/command-internal/path-containment.ts index 5c23981748..1359d05a6e 100644 --- a/apps/cli/src/command-internal/path-containment.ts +++ b/apps/cli/src/command-internal/path-containment.ts @@ -4,10 +4,9 @@ * `content_path` values and the `supabase/notebooks/` directory. */ -import { lstatSync, readlinkSync, realpathSync, type Stats } from "node:fs"; -import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { Effect, FileSystem, Option, Path, type PlatformError, Predicate, Result } from "effect"; -// `readlinkSync` bypasses the OS's own `ELOOP` symlink-cycle detection when +// `readLink` bypasses the OS's own `ELOOP` symlink-cycle detection when // manually following a dangling/unsearchable/looping symlink one hop at a // time (see `canonicalizeExistingPath` below), so that manual follow needs // its own explicit bound. @@ -20,84 +19,94 @@ const MAX_SYMLINK_FOLLOW_DEPTH = 40; * happens to start with two dots (e.g. `..templates`) is a distinct, * in-root path and must not be rejected. */ -export function isPathContainedInRoot(root: string, candidatePath: string): boolean { - const rel = relative(root, candidatePath); - return rel === "" || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`)); +export function isPathContainedInRoot( + path: Path.Path, + root: string, + candidatePath: string, +): boolean { + const rel = path.relative(root, candidatePath); + return rel === "" || (!path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`)); } /** - * Canonicalizes `path` when it exists (per `lstatSync`), or returns `undefined` so + * Canonicalizes `path` when it exists (per `realPath`/`readLink`), or returns `undefined` so * {@link canonicalPathForContainment} keeps walking up to an existing ancestor. * - * `realpathSync` can throw for a path that exists (dangling symlink, `EACCES` target, `ELOOP`), + * `realPath` can fail for a path that exists (dangling symlink, `EACCES` target, `ELOOP`), * so such a symlink is followed one hop by hand, bounded by {@link MAX_SYMLINK_FOLLOW_DEPTH}, * and its target canonicalized in turn; a chain still unresolved at the bound returns the lexical - * path so a loop is rejected rather than accepted. An `lstatSync` failure unrelated to the path + * path so a loop is rejected rather than accepted. A `readLink` failure unrelated to the path * itself (unreadable ancestor, over-long name) counts as "doesn't exist yet", so an honest in-root * path behind a restricted ancestor isn't falsely rejected. */ -function canonicalizeExistingPath(path: string, depth: number): string | undefined { - try { - return realpathSync(path); - } catch { - // Wraps only the `lstatSync` call, not the recursive canonicalization below it: including - // that would let a deep throw there return the outer symlink's own lexically-in-root path, - // turning a rejection into an accept. - let entry: Stats | undefined; - try { - entry = lstatSync(path, { throwIfNoEntry: false }); - } catch { - return undefined; +const canonicalizeExistingPath = Effect.fnUntraced(function* (path: string, depth: number) { + const fs = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + const canonical = yield* fs.realPath(path).pipe(Effect.option); + if (Option.isSome(canonical)) return canonical.value; + /* `FileSystem` has no `lstat`, so `readLink` stands in: it succeeds only for a symlink, and a + symlink whose own mode denies reading it fails with `EACCES` under a searchable parent. */ + const link = yield* fs.readLink(path).pipe(Effect.result); + const isSymbolicLink = + Result.isSuccess(link) || + (Predicate.isTagged(link.failure.reason, "PermissionDenied") && + (yield* fs.stat(`${pathService.dirname(path)}${pathService.sep}.`).pipe(Effect.isSuccess))); + if (isSymbolicLink) { + if (depth < MAX_SYMLINK_FOLLOW_DEPTH) { + const target = yield* Effect.fromResult(link); + return yield* canonicalPathForContainment( + pathService.isAbsolute(target) + ? target + : pathService.join(pathService.dirname(path), target), + depth + 1, + ); } - if (entry === undefined) return undefined; - if (entry.isSymbolicLink()) { - if (depth < MAX_SYMLINK_FOLLOW_DEPTH) { - const target = readlinkSync(path); - return canonicalPathForContainment( - isAbsolute(target) ? target : join(dirname(path), target), - depth + 1, - ); - } - // Must not be treated as "doesn't exist": returning `undefined` here would let the - // ancestor walk-up canonicalize past the whole unresolvable loop and silently accept it - // instead of failing closed. - return path; - } - // A non-symlink entry `lstat` can see but `realpath` can't resolve — e.g. a chmod-000 - // directory on Darwin, whose realpath(3) needs search permission on itself, not just its - // parent. Deferred to the same "doesn't exist yet" ancestor walk-up as a genuinely missing - // path, since a plain entry can't recurse into a loop. - return undefined; + // Must not be treated as "doesn't exist": returning `undefined` here would let the + // ancestor walk-up canonicalize past the whole unresolvable loop and silently accept it + // instead of failing closed. + return path; } -} + // A non-symlink entry that exists but `realPath` can't resolve — e.g. a chmod-000 + // directory on Darwin, whose realpath(3) needs search permission on itself, not just its + // parent. Deferred to the same "doesn't exist yet" ancestor walk-up as a genuinely missing + // path, since a plain entry can't recurse into a loop. + return undefined; +}); /** * Canonicalizes `path` for the containment check, tolerating a path (or an ancestor of it) * that genuinely doesn't exist yet — the normal case for a missing template file or a * notebooks directory a pull is about to create, which should surface as a missing-file error, * not a containment error. Walks up to the deepest existing ancestor, resolves it with - * `realpathSync` (dereferencing any symlinks, including a symlinked project root itself), then + * `realPath` (dereferencing any symlinks, including a symlinked project root itself), then * re-appends the missing tail lexically. The walk-up is iterative, not recursive, so it stays * correct against a pathologically long chain of missing ancestors; each ancestor still goes * through {@link canonicalizeExistingPath}, so an intermediate dangling/unsearchable/looping * symlink is followed rather than lexically skipped. */ -export function canonicalPathForContainment(path: string, depth = 0): string { - const canonical = canonicalizeExistingPath(path, depth); - if (canonical !== undefined) return canonical; +export const canonicalPathForContainment: ( + path: string, + depth?: number, +) => Effect.Effect = + Effect.fnUntraced(function* (path: string, depth = 0) { + const pathService = yield* Path.Path; + const canonical = yield* canonicalizeExistingPath(path, depth); + if (canonical !== undefined) return canonical; - const tail: string[] = [basename(path)]; - let current = dirname(path); - for (;;) { - const ancestorCanonical = canonicalizeExistingPath(current, depth); - if (ancestorCanonical !== undefined) { - return tail.reduceRight((acc, name) => join(acc, name), ancestorCanonical); - } - const parent = dirname(current); - if (parent === current) { - return resolve(tail.reduceRight((acc, name) => join(acc, name), current)); + const tail: string[] = [pathService.basename(path)]; + let current = pathService.dirname(path); + for (;;) { + const ancestorCanonical = yield* canonicalizeExistingPath(current, depth); + if (ancestorCanonical !== undefined) { + return tail.reduceRight((acc, name) => pathService.join(acc, name), ancestorCanonical); + } + const parent = pathService.dirname(current); + if (parent === current) { + return pathService.resolve( + tail.reduceRight((acc, name) => pathService.join(acc, name), current), + ); + } + tail.push(pathService.basename(current)); + current = parent; } - tail.push(basename(current)); - current = parent; - } -} + }); diff --git a/apps/cli/src/command-internal/stack-config.ts b/apps/cli/src/command-internal/stack-config.ts index 2a843e74e1..5e897e7d05 100644 --- a/apps/cli/src/command-internal/stack-config.ts +++ b/apps/cli/src/command-internal/stack-config.ts @@ -210,13 +210,13 @@ const EDGE_RUNTIME_INSPECTOR_PORT: StackEndpointSetting = { }; /** The configured port when the setting is present in `config.toml` or its env var, else undefined. */ -const resolvePort = ( +const resolvePort = Effect.fnUntraced(function* ( setting: StackEndpointSetting, document: Readonly> | undefined, configured: number, env: Readonly>, -): number | undefined => { - if (envOverride(setting.envVar, undefined, env) !== undefined) return configured; +) { + if ((yield* envOverride(setting.envVar, undefined, env)) !== undefined) return configured; const path = setting.configPath.split("."); const key = path[path.length - 1] ?? ""; const parent = path @@ -226,7 +226,7 @@ const resolvePort = ( document, ); return typeof parent?.[key] === "number" ? configured : undefined; -}; +}); /** * Maps a saved stack endpoint (service + endpoint name) to the `StackEndpointSetting` that controls it. @@ -294,26 +294,27 @@ const authProviderNames = [ "zoom", ] as const; -const resolvedPort = ( +const resolvedPort = Effect.fnUntraced(function* ( name: string, configured: number, field: string, env: Readonly>, -) => - envOverride(name, undefined, env) === undefined +) { + return (yield* envOverride(name, undefined, env)) === undefined ? configured - : envOverridePort(name, configured, field, env); + : yield* envOverridePort(name, configured, field, env); +}); -const resolveAuthOverrides = ( +const resolveAuthOverrides = Effect.fnUntraced(function* ( auth: CliConfig["auth"], document: Readonly> | undefined, env: Readonly>, -) => { +) { const authDocument = section(document, "auth"); - const email = resolveAuthEmail(auth.email, authDocument, env); - const smtp = resolveAuthEmailSmtp(authDocument, env); - const hooks = resolveAuthHooks(authDocument, auth.hook, env); - const external = resolveAuthExternalProviders(authDocument, auth.external, env); + const email = yield* resolveAuthEmail(auth.email, authDocument, env); + const smtp = yield* resolveAuthEmailSmtp(authDocument, env); + const hooks = yield* resolveAuthHooks(authDocument, auth.hook, env); + const external = yield* resolveAuthExternalProviders(authDocument, auth.external, env); const externalResolved = { ...auth.external }; for (const name of authProviderNames) { const value = external[name]; @@ -381,7 +382,7 @@ const resolveAuthOverrides = ( template: resolvedEmailTemplate, notification: resolvedEmailNotification, }; - const additionalRedirectUrls = envOverride( + const additionalRedirectUrls = yield* envOverride( "SUPABASE_AUTH_ADDITIONAL_REDIRECT_URLS", undefined, env, @@ -398,73 +399,80 @@ const resolveAuthOverrides = ( admin_email: smtp.adminEmail, sender_name: smtp.senderName, }; - const resolvedSms = resolveAuthSms(authDocument, auth.sms, env); + const resolvedSms = yield* resolveAuthSms(authDocument, auth.sms, env); return { ...auth, - enabled: envOverrideBool("SUPABASE_AUTH_ENABLED", auth.enabled, "auth.enabled", env), - site_url: envOverride("SUPABASE_AUTH_SITE_URL", auth.site_url, env), + enabled: yield* envOverrideBool("SUPABASE_AUTH_ENABLED", auth.enabled, "auth.enabled", env), + site_url: yield* envOverride("SUPABASE_AUTH_SITE_URL", auth.site_url, env), additional_redirect_urls: additionalRedirectUrls === undefined ? auth.additional_redirect_urls : strToArr(additionalRedirectUrls), - jwt_expiry: envOverrideUint( + jwt_expiry: yield* envOverrideUint( "SUPABASE_AUTH_JWT_EXPIRY", "auth.jwt_expiry", auth.jwt_expiry, env, ), - enable_refresh_token_rotation: envOverrideBool( + enable_refresh_token_rotation: yield* envOverrideBool( "SUPABASE_AUTH_ENABLE_REFRESH_TOKEN_ROTATION", auth.enable_refresh_token_rotation, "auth.enable_refresh_token_rotation", env, ), - refresh_token_reuse_interval: envOverrideUint( + refresh_token_reuse_interval: yield* envOverrideUint( "SUPABASE_AUTH_REFRESH_TOKEN_REUSE_INTERVAL", "auth.refresh_token_reuse_interval", auth.refresh_token_reuse_interval, env, ), - enable_manual_linking: envOverrideBool( + enable_manual_linking: yield* envOverrideBool( "SUPABASE_AUTH_ENABLE_MANUAL_LINKING", auth.enable_manual_linking, "auth.enable_manual_linking", env, ), - enable_signup: envOverrideBool( + enable_signup: yield* envOverrideBool( "SUPABASE_AUTH_ENABLE_SIGNUP", auth.enable_signup, "auth.enable_signup", env, ), - enable_anonymous_sign_ins: envOverrideBool( + enable_anonymous_sign_ins: yield* envOverrideBool( "SUPABASE_AUTH_ENABLE_ANONYMOUS_SIGN_INS", auth.enable_anonymous_sign_ins, "auth.enable_anonymous_sign_ins", env, ), - minimum_password_length: envOverrideUint( + minimum_password_length: yield* envOverrideUint( "SUPABASE_AUTH_MINIMUM_PASSWORD_LENGTH", "auth.minimum_password_length", auth.minimum_password_length, env, ), - password_requirements: envOverrideAuthPasswordRequirements(auth.password_requirements, env), - publishable_key: envOverride("SUPABASE_AUTH_PUBLISHABLE_KEY", auth.publishable_key, env), - secret_key: envOverride("SUPABASE_AUTH_SECRET_KEY", auth.secret_key, env), - anon_key: envOverride("SUPABASE_AUTH_ANON_KEY", auth.anon_key, env), - service_role_key: envOverride("SUPABASE_AUTH_SERVICE_ROLE_KEY", auth.service_role_key, env), - rate_limit: resolveGotrueRateLimit(auth.rate_limit, env), - captcha: resolveAuthCaptcha(authDocument, auth.captcha, env), + password_requirements: yield* envOverrideAuthPasswordRequirements( + auth.password_requirements, + env, + ), + publishable_key: yield* envOverride("SUPABASE_AUTH_PUBLISHABLE_KEY", auth.publishable_key, env), + secret_key: yield* envOverride("SUPABASE_AUTH_SECRET_KEY", auth.secret_key, env), + anon_key: yield* envOverride("SUPABASE_AUTH_ANON_KEY", auth.anon_key, env), + service_role_key: yield* envOverride( + "SUPABASE_AUTH_SERVICE_ROLE_KEY", + auth.service_role_key, + env, + ), + rate_limit: yield* resolveGotrueRateLimit(auth.rate_limit, env), + captcha: yield* resolveAuthCaptcha(authDocument, auth.captcha, env), hook: hookResolved, - mfa: resolveAuthMfa(auth.mfa, env), - sessions: resolveGotrueSessions(auth.sessions, env), + mfa: yield* resolveAuthMfa(auth.mfa, env), + sessions: yield* resolveGotrueSessions(auth.sessions, env), email: { ...resolvedEmail, smtp: resolvedSmtp }, sms: { ...resolvedSms, twilio: { ...resolvedSms.twilio, - content_sid: envOverride( + content_sid: yield* envOverride( "SUPABASE_AUTH_SMS_TWILIO_CONTENT_SID", auth.sms.twilio.content_sid, env, @@ -472,16 +480,16 @@ const resolveAuthOverrides = ( }, }, external: externalResolved, - web3: resolveGotrueWeb3(auth.web3, env), - oauth_server: resolveGotrueOAuthServer(auth.oauth_server, env), + web3: yield* resolveGotrueWeb3(auth.web3, env), + oauth_server: yield* resolveGotrueOAuthServer(auth.oauth_server, env), }; -}; +}); -const resolveEffectiveCliConfig = ( +const resolveEffectiveCliConfig = Effect.fnUntraced(function* ( config: CliConfig, document: Readonly> | undefined, env: Readonly>, -): CliConfig => { +) { const api = config.api; const db = config.db; const storage = config.storage; @@ -492,10 +500,14 @@ const resolveEffectiveCliConfig = ( const pooler = db.pooler; const edge = config.edge_runtime; const experimental = config.experimental; - const apiSchemasOverride = envOverride("SUPABASE_API_SCHEMAS", undefined, env); - const apiExtraSearchPathOverride = envOverride("SUPABASE_API_EXTRA_SEARCH_PATH", undefined, env); + const apiSchemasOverride = yield* envOverride("SUPABASE_API_SCHEMAS", undefined, env); + const apiExtraSearchPathOverride = yield* envOverride( + "SUPABASE_API_EXTRA_SEARCH_PATH", + undefined, + env, + ); const imageDocument = section(section(document, "storage"), "image_transformation"); - const imageEnabledOverride = envOverride( + const imageEnabledOverride = yield* envOverride( "SUPABASE_STORAGE_IMAGE_TRANSFORMATION_ENABLED", undefined, env, @@ -504,25 +516,30 @@ const resolveEffectiveCliConfig = ( imageDocument?.enabled !== undefined || imageEnabledOverride !== undefined; const resolvedApi = { ...api, - enabled: envOverrideBool("SUPABASE_API_ENABLED", api.enabled, "api.enabled", env), - port: resolvedPort("SUPABASE_API_PORT", api.port, "api.port", env), + enabled: yield* envOverrideBool("SUPABASE_API_ENABLED", api.enabled, "api.enabled", env), + port: yield* resolvedPort("SUPABASE_API_PORT", api.port, "api.port", env), schemas: apiSchemasOverride === undefined ? api.schemas : strToArr(apiSchemasOverride), extra_search_path: apiExtraSearchPathOverride === undefined ? api.extra_search_path : strToArr(apiExtraSearchPathOverride), - max_rows: envOverrideApiMaxRows(api.max_rows, env), + max_rows: yield* envOverrideApiMaxRows(api.max_rows, env), tls: { ...api.tls, - enabled: envOverrideBool("SUPABASE_API_TLS_ENABLED", api.tls.enabled, "api.tls.enabled", env), - cert_path: envOverride("SUPABASE_API_TLS_CERT_PATH", api.tls.cert_path, env), - key_path: envOverride("SUPABASE_API_TLS_KEY_PATH", api.tls.key_path, env), + enabled: yield* envOverrideBool( + "SUPABASE_API_TLS_ENABLED", + api.tls.enabled, + "api.tls.enabled", + env, + ), + cert_path: yield* envOverride("SUPABASE_API_TLS_CERT_PATH", api.tls.cert_path, env), + key_path: yield* envOverride("SUPABASE_API_TLS_KEY_PATH", api.tls.key_path, env), }, - external_url: envOverride("SUPABASE_API_EXTERNAL_URL", api.external_url, env), + external_url: yield* envOverride("SUPABASE_API_EXTERNAL_URL", api.external_url, env), auto_expose_new_tables: api.auto_expose_new_tables !== undefined || - envOverride("SUPABASE_API_AUTO_EXPOSE_NEW_TABLES", undefined, env) !== undefined - ? envOverrideBool( + (yield* envOverride("SUPABASE_API_AUTO_EXPOSE_NEW_TABLES", undefined, env)) !== undefined + ? yield* envOverrideBool( "SUPABASE_API_AUTO_EXPOSE_NEW_TABLES", api.auto_expose_new_tables ?? false, "api.auto_expose_new_tables", @@ -532,8 +549,13 @@ const resolveEffectiveCliConfig = ( }; const resolvedStorage = { ...storage, - enabled: envOverrideBool("SUPABASE_STORAGE_ENABLED", storage.enabled, "storage.enabled", env), - file_size_limit: envOverride( + enabled: yield* envOverrideBool( + "SUPABASE_STORAGE_ENABLED", + storage.enabled, + "storage.enabled", + env, + ), + file_size_limit: yield* envOverride( "SUPABASE_STORAGE_FILE_SIZE_LIMIT", String(storage.file_size_limit), env, @@ -541,7 +563,7 @@ const resolveEffectiveCliConfig = ( image_transformation: imageEnabledExplicit ? { ...storage.image_transformation, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_STORAGE_IMAGE_TRANSFORMATION_ENABLED", storage.image_transformation?.enabled ?? false, "storage.image_transformation.enabled", @@ -551,7 +573,7 @@ const resolveEffectiveCliConfig = ( : undefined, s3_protocol: { ...storage.s3_protocol, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_STORAGE_S3_PROTOCOL_ENABLED", storage.s3_protocol.enabled, "storage.s3_protocol.enabled", @@ -560,25 +582,25 @@ const resolveEffectiveCliConfig = ( }, analytics: { ...storage.analytics, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_STORAGE_ANALYTICS_ENABLED", storage.analytics.enabled, "storage.analytics.enabled", env, ), - max_namespaces: envOverrideUint( + max_namespaces: yield* envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_NAMESPACES", "storage.analytics.max_namespaces", storage.analytics.max_namespaces, env, ), - max_tables: envOverrideUint( + max_tables: yield* envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_TABLES", "storage.analytics.max_tables", storage.analytics.max_tables, env, ), - max_catalogs: envOverrideUint( + max_catalogs: yield* envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_CATALOGS", "storage.analytics.max_catalogs", storage.analytics.max_catalogs, @@ -587,19 +609,19 @@ const resolveEffectiveCliConfig = ( }, vector: { ...storage.vector, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_STORAGE_VECTOR_ENABLED", storage.vector.enabled, "storage.vector.enabled", env, ), - max_buckets: envOverrideUint( + max_buckets: yield* envOverrideUint( "SUPABASE_STORAGE_VECTOR_MAX_BUCKETS", "storage.vector.max_buckets", storage.vector.max_buckets, env, ), - max_indexes: envOverrideUint( + max_indexes: yield* envOverrideUint( "SUPABASE_STORAGE_VECTOR_MAX_INDEXES", "storage.vector.max_indexes", storage.vector.max_indexes, @@ -609,15 +631,15 @@ const resolveEffectiveCliConfig = ( }; const resolvedEdge = { ...edge, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_EDGE_RUNTIME_ENABLED", edge.enabled, "edge_runtime.enabled", env, ), - policy: envOverrideEdgeRuntimePolicy(edge.policy, env), - deno_version: envOverrideDenoVersion(edge.deno_version, env), - inspector_port: resolvedPort( + policy: yield* envOverrideEdgeRuntimePolicy(edge.policy, env), + deno_version: yield* envOverrideDenoVersion(edge.deno_version, env), + inspector_port: yield* resolvedPort( "SUPABASE_EDGE_RUNTIME_INSPECTOR_PORT", edge.inspector_port, "edge_runtime.inspector_port", @@ -626,36 +648,44 @@ const resolveEffectiveCliConfig = ( }; const resolvedAnalytics = { ...analytics, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_ANALYTICS_ENABLED", analytics.enabled, "analytics.enabled", env, ), - port: resolvedPort("SUPABASE_ANALYTICS_PORT", analytics.port, "analytics.port", env), - backend: envOverrideAnalyticsBackend(analytics.backend, env), - gcp_project_id: envOverride("SUPABASE_ANALYTICS_GCP_PROJECT_ID", analytics.gcp_project_id, env), - gcp_project_number: envOverride( + port: yield* resolvedPort("SUPABASE_ANALYTICS_PORT", analytics.port, "analytics.port", env), + backend: yield* envOverrideAnalyticsBackend(analytics.backend, env), + gcp_project_id: yield* envOverride( + "SUPABASE_ANALYTICS_GCP_PROJECT_ID", + analytics.gcp_project_id, + env, + ), + gcp_project_number: yield* envOverride( "SUPABASE_ANALYTICS_GCP_PROJECT_NUMBER", analytics.gcp_project_number, env, ), - gcp_jwt_path: envOverride("SUPABASE_ANALYTICS_GCP_JWT_PATH", analytics.gcp_jwt_path, env), + gcp_jwt_path: yield* envOverride( + "SUPABASE_ANALYTICS_GCP_JWT_PATH", + analytics.gcp_jwt_path, + env, + ), }; const resolvedPooler = { ...pooler, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_DB_POOLER_ENABLED", pooler.enabled, "db.pooler.enabled", env, ), - port: resolvedPort("SUPABASE_DB_POOLER_PORT", pooler.port, "db.pooler.port", env), - pool_mode: envOverridePoolMode(pooler.pool_mode, env), - default_pool_size: envOverrideDefaultPoolSize(pooler.default_pool_size, env), - max_client_conn: envOverrideMaxClientConn(pooler.max_client_conn, env), + port: yield* resolvedPort("SUPABASE_DB_POOLER_PORT", pooler.port, "db.pooler.port", env), + pool_mode: yield* envOverridePoolMode(pooler.pool_mode, env), + default_pool_size: yield* envOverrideDefaultPoolSize(pooler.default_pool_size, env), + max_client_conn: yield* envOverrideMaxClientConn(pooler.max_client_conn, env), }; - const authEnabled = envOverrideBool( + const authEnabled = yield* envOverrideBool( "SUPABASE_AUTH_ENABLED", config.auth.enabled, "auth.enabled", @@ -663,75 +693,75 @@ const resolveEffectiveCliConfig = ( ); const thirdParty = { firebase: { - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_ENABLED", config.auth.third_party.firebase.enabled, "auth.third_party.firebase.enabled", env, ), - project_id: envOverride( + project_id: yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_FIREBASE_PROJECT_ID", config.auth.third_party.firebase.project_id, env, ), }, auth0: { - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_AUTH0_ENABLED", config.auth.third_party.auth0.enabled, "auth.third_party.auth0.enabled", env, ), - tenant: envOverride( + tenant: yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT", config.auth.third_party.auth0.tenant, env, ), - tenant_region: envOverride( + tenant_region: yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_AUTH0_TENANT_REGION", config.auth.third_party.auth0.tenant_region, env, ), }, aws_cognito: { - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_ENABLED", config.auth.third_party.aws_cognito.enabled, "auth.third_party.aws_cognito.enabled", env, ), - user_pool_id: envOverride( + user_pool_id: yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_ID", config.auth.third_party.aws_cognito.user_pool_id, env, ), - user_pool_region: envOverride( + user_pool_region: yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_AWS_COGNITO_USER_POOL_REGION", config.auth.third_party.aws_cognito.user_pool_region, env, ), }, clerk: { - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_CLERK_ENABLED", config.auth.third_party.clerk.enabled, "auth.third_party.clerk.enabled", env, ), - domain: envOverride( + domain: yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_CLERK_DOMAIN", config.auth.third_party.clerk.domain, env, ), }, workos: { - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ENABLED", config.auth.third_party.workos.enabled, "auth.third_party.workos.enabled", env, ), - issuer_url: envOverride( + issuer_url: yield* envOverride( "SUPABASE_AUTH_THIRD_PARTY_WORKOS_ISSUER_URL", config.auth.third_party.workos.issuer_url, env, @@ -741,17 +771,17 @@ const resolveEffectiveCliConfig = ( // JWT security settings apply to non-Auth workloads too, so resolve them // regardless of whether the Auth capability is enabled. const authResolved = { - ...(authEnabled ? resolveAuthOverrides(config.auth, document, env) : config.auth), + ...(authEnabled ? yield* resolveAuthOverrides(config.auth, document, env) : config.auth), enabled: authEnabled, third_party: thirdParty, - jwt_issuer: envOverride("SUPABASE_AUTH_JWT_ISSUER", config.auth.jwt_issuer, env), - signing_keys_path: envOverride( + jwt_issuer: yield* envOverride("SUPABASE_AUTH_JWT_ISSUER", config.auth.jwt_issuer, env), + signing_keys_path: yield* envOverride( "SUPABASE_AUTH_SIGNING_KEYS_PATH", config.auth.signing_keys_path, env, ), - jwt_secret: decryptAuthSecret( - envOverride("SUPABASE_AUTH_JWT_SECRET", config.auth.jwt_secret, env), + jwt_secret: yield* decryptAuthSecret( + yield* envOverride("SUPABASE_AUTH_JWT_SECRET", config.auth.jwt_secret, env), env, ), }; @@ -761,25 +791,25 @@ const resolveEffectiveCliConfig = ( auth: authResolved, db: { ...db, - port: resolvedPort("SUPABASE_DB_PORT", db.port, "db.port", env), - major_version: envOverrideMajorVersion(db.major_version, env), - health_timeout: envOverride("SUPABASE_DB_HEALTH_TIMEOUT", db.health_timeout, env), - orioledb_version: envOverrideOrioleDBVersion(db.orioledb_version, env), - settings: resolveDbSettingsEnvOverrides(db.settings, env), + port: yield* resolvedPort("SUPABASE_DB_PORT", db.port, "db.port", env), + major_version: yield* envOverrideMajorVersion(db.major_version, env), + health_timeout: yield* envOverride("SUPABASE_DB_HEALTH_TIMEOUT", db.health_timeout, env), + orioledb_version: yield* envOverrideOrioleDBVersion(db.orioledb_version, env), + settings: yield* resolveDbSettingsEnvOverrides(db.settings, env), pooler: resolvedPooler, }, edge_runtime: resolvedEdge, // Same S3 overrides legacy Compose applies, so the OrioleDB S3 guard sees env-only values. experimental: { ...experimental, - s3_host: envOverride("SUPABASE_EXPERIMENTAL_S3_HOST", experimental.s3_host, env), - s3_region: envOverride("SUPABASE_EXPERIMENTAL_S3_REGION", experimental.s3_region, env), - s3_access_key: envOverride( + s3_host: yield* envOverride("SUPABASE_EXPERIMENTAL_S3_HOST", experimental.s3_host, env), + s3_region: yield* envOverride("SUPABASE_EXPERIMENTAL_S3_REGION", experimental.s3_region, env), + s3_access_key: yield* envOverride( "SUPABASE_EXPERIMENTAL_S3_ACCESS_KEY", experimental.s3_access_key, env, ), - s3_secret_key: envOverride( + s3_secret_key: yield* envOverride( "SUPABASE_EXPERIMENTAL_S3_SECRET_KEY", experimental.s3_secret_key, env, @@ -787,53 +817,58 @@ const resolveEffectiveCliConfig = ( }, realtime: { ...realtime, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_REALTIME_ENABLED", realtime.enabled, "realtime.enabled", env, ), - ip_version: envOverrideRealtimeIpVersion(realtime.ip_version, env), - max_header_length: envOverrideRealtimeMaxHeaderLength(realtime.max_header_length, env), + ip_version: yield* envOverrideRealtimeIpVersion(realtime.ip_version, env), + max_header_length: yield* envOverrideRealtimeMaxHeaderLength(realtime.max_header_length, env), }, storage: resolvedStorage, analytics: resolvedAnalytics, studio: { ...studio, - enabled: envOverrideBool("SUPABASE_STUDIO_ENABLED", studio.enabled, "studio.enabled", env), - port: resolvedPort("SUPABASE_STUDIO_PORT", studio.port, "studio.port", env), - api_url: envOverride("SUPABASE_STUDIO_API_URL", studio.api_url, env), - openai_api_key: decryptAuthSecret( - envOverride("SUPABASE_STUDIO_OPENAI_API_KEY", studio.openai_api_key, env), + enabled: yield* envOverrideBool( + "SUPABASE_STUDIO_ENABLED", + studio.enabled, + "studio.enabled", + env, + ), + port: yield* resolvedPort("SUPABASE_STUDIO_PORT", studio.port, "studio.port", env), + api_url: yield* envOverride("SUPABASE_STUDIO_API_URL", studio.api_url, env), + openai_api_key: yield* decryptAuthSecret( + yield* envOverride("SUPABASE_STUDIO_OPENAI_API_KEY", studio.openai_api_key, env), env, ), }, local_smtp: { ...mail, - enabled: envOverrideBool( + enabled: yield* envOverrideBool( "SUPABASE_LOCAL_SMTP_ENABLED", mail.enabled, "local_smtp.enabled", env, ), - port: resolvedPort("SUPABASE_LOCAL_SMTP_PORT", mail.port, "local_smtp.port", env), - smtp_port: resolvedPort( + port: yield* resolvedPort("SUPABASE_LOCAL_SMTP_PORT", mail.port, "local_smtp.port", env), + smtp_port: yield* resolvedPort( "SUPABASE_LOCAL_SMTP_SMTP_PORT", mail.smtp_port ?? 0, "local_smtp.smtp_port", env, ), - pop3_port: resolvedPort( + pop3_port: yield* resolvedPort( "SUPABASE_LOCAL_SMTP_POP3_PORT", mail.pop3_port ?? 0, "local_smtp.pop3_port", env, ), - admin_email: envOverride("SUPABASE_LOCAL_SMTP_ADMIN_EMAIL", mail.admin_email, env), - sender_name: envOverride("SUPABASE_LOCAL_SMTP_SENDER_NAME", mail.sender_name, env), + admin_email: yield* envOverride("SUPABASE_LOCAL_SMTP_ADMIN_EMAIL", mail.admin_email, env), + sender_name: yield* envOverride("SUPABASE_LOCAL_SMTP_SENDER_NAME", mail.sender_name, env), }, - }; -}; + } satisfies CliConfig; +}); const unsupportedConfigPaths = [ { path: "api.tls", active: (config: CliConfig) => config.api.enabled }, @@ -947,18 +982,18 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( projectRoot, (message) => new StackConfigError({ message }), )); - const effectiveInput = yield* Effect.try({ - try: () => - resolveEffectiveCliConfig( - context.config, - context.loaded?.document, - context.projectEnvValues, - ), - catch: (cause) => - new StackConfigError({ - message: cause instanceof Error ? cause.message : "invalid config overrides", - }), - }); + const effectiveInput = yield* resolveEffectiveCliConfig( + context.config, + context.loaded?.document, + context.projectEnvValues, + ).pipe( + Effect.mapError( + (cause) => + new StackConfigError({ + message: cause instanceof Error ? cause.message : "invalid config overrides", + }), + ), + ); const validatedConfig = yield* validateCliConfig(withoutUndefined(effectiveInput)).pipe( Effect.mapError((cause) => { const issues = SchemaIssue.makeFormatterStandardSchemaV1({ @@ -982,29 +1017,32 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( return yield* new StackConfigError({ message: validationError }); if (context.loaded === null) yield* rejectFrozenWithoutConfig(projectRoot, opts?.frozen === true); - const externalProviders = yield* Effect.try({ - try: () => - resolveAuthExternalProviders( - section(context.loaded?.document, "auth"), - validatedConfig.auth.external, - context.projectEnvValues, - ), - catch: (cause) => - new StackConfigError({ - message: cause instanceof Error ? cause.message : "invalid auth provider config", - }), - }); + const externalProviders = yield* resolveAuthExternalProviders( + section(context.loaded?.document, "auth"), + validatedConfig.auth.external, + context.projectEnvValues, + ).pipe( + Effect.mapError( + (cause) => + new StackConfigError({ + message: cause instanceof Error ? cause.message : "invalid auth provider config", + }), + ), + ); const authConfig = yield* resolveAuthConfig( validatedConfig.auth, validatedConfig.local_smtp, { - authExternalUrl: resolveAuthExternalUrl( + authExternalUrl: yield* resolveAuthExternalUrl( context.loaded?.document, context.projectEnvValues, ), apiExternalUrl: validatedConfig.api.external_url, externalProviders, - ...resolveGotruePasskeyWebauthn(context.loaded?.document, context.projectEnvValues), + ...(yield* resolveGotruePasskeyWebauthn( + context.loaded?.document, + context.projectEnvValues, + ).pipe(Effect.orDie)), }, ); const path = yield* Path.Path; @@ -1022,29 +1060,36 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( message: cause instanceof Error ? cause.message : String(cause), }), }); + const toStackConfigError = (cause: unknown) => + cause instanceof StackConfigError + ? cause + : new StackConfigError({ + message: cause instanceof Error ? cause.message : String(cause), + }); const localKeys = yield* Effect.cached( - Effect.try({ - try: () => { - const configured = (value: string | undefined) => - value === undefined || value === "" - ? undefined - : decryptAuthSecret(value, context.projectEnvValues); - const publishableKey = configured(auth.publishable_key); - const secretKey = configured(auth.secret_key); - const configuredAnonKey = configured(auth.anon_key); - const configuredServiceRoleKey = configured(auth.service_role_key); - const configuredSigningKeys = resolveConfiguredSigningKeys( - validatedConfig, - projectRoot, - context.projectEnvValues, - ); - const signingKeys = - configuredSigningKeys ?? - (auth.signing_keys_path === undefined || auth.signing_keys_path.length === 0 - ? undefined - : [DEFAULT_SIGNING_KEY]); - const signingKey = signingKeys?.[0]; - return { + Effect.gen(function* () { + const configured = Effect.fnUntraced(function* (value: string | undefined) { + return value === undefined || value === "" + ? undefined + : yield* decryptAuthSecret(value, context.projectEnvValues); + }); + const publishableKey = yield* configured(auth.publishable_key); + const secretKey = yield* configured(auth.secret_key); + const configuredAnonKey = yield* configured(auth.anon_key); + const configuredServiceRoleKey = yield* configured(auth.service_role_key); + const configuredSigningKeys = yield* resolveConfiguredSigningKeys( + validatedConfig, + projectRoot, + context.projectEnvValues, + ); + const signingKeys = + configuredSigningKeys ?? + (auth.signing_keys_path === undefined || auth.signing_keys_path.length === 0 + ? undefined + : [DEFAULT_SIGNING_KEY]); + const signingKey = signingKeys?.[0]; + return yield* Effect.try({ + try: () => ({ ...(publishableKey === undefined ? {} : { publishableKey }), ...(secretKey === undefined ? {} : { secretKey }), ...(configuredAnonKey === undefined @@ -1065,13 +1110,14 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( gotrueJwtKeys: encodeJwkArray(signingKeys), publicSigningKeys: encodeJwkArray(signingKeys.map(toPublicJwk)), }), - }; - }, - catch: (cause) => - new StackConfigError({ - message: cause instanceof Error ? cause.message : String(cause), }), - }), + catch: toStackConfigError, + }); + }).pipe( + Effect.mapError(toStackConfigError), + Effect.provideService(FileSystem.FileSystem, yield* FileSystem.FileSystem), + Effect.provideService(Path.Path, path), + ), ); const remoteJwks = Effect.gen(function* () { return issuer === undefined @@ -1151,16 +1197,15 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( ), catch: (cause) => new StackConfigError({ message: String(cause) }), }); - const functionsEnv = yield* Effect.try({ - try: () => - Object.fromEntries( - Object.entries(validatedConfig.edge_runtime.secrets ?? {}).map(([key, value]) => [ - key, - decryptAuthSecret(value, context.projectEnvValues) ?? "", - ]), - ), - catch: (cause) => new StackConfigError({ message: String(cause) }), - }); + const functionsEnv = Object.fromEntries( + yield* Effect.forEach( + Object.entries(validatedConfig.edge_runtime.secrets ?? {}), + ([key, value]) => + decryptAuthSecret(value, context.projectEnvValues).pipe( + Effect.map((decrypted) => [key, decrypted ?? ""] as const), + ), + ).pipe(Effect.mapError((cause) => new StackConfigError({ message: String(cause) }))), + ); const storageFileSizeLimit = yield* Effect.try({ try: () => String(parseFileSizeLimit(validatedConfig.storage.file_size_limit)), catch: (cause) => @@ -1171,72 +1216,68 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( catch: (cause) => new StackConfigError({ message: `Invalid db.health_timeout: ${String(cause)}` }), }); - const configuredJwtSecret = yield* Effect.try({ - try: () => - validatedConfig.auth.jwt_secret === undefined || validatedConfig.auth.jwt_secret === "" - ? undefined - : resolveJwtSecret(validatedConfig.auth.jwt_secret), - catch: (cause) => new StackConfigError({ message: String(cause) }), - }); + const configuredJwtSecret = + validatedConfig.auth.jwt_secret === undefined || validatedConfig.auth.jwt_secret === "" + ? undefined + : yield* resolveJwtSecret(validatedConfig.auth.jwt_secret).pipe( + Effect.mapError((cause) => new StackConfigError({ message: String(cause) })), + ); const jwtSecret = configuredJwtSecret === undefined ? undefined : Redacted.make(configuredJwtSecret); const document = context.loaded?.document; - const rootKey = yield* Effect.try({ - try: () => { - const raw = section(document, "db")?.root_key; - if (raw !== undefined && typeof raw !== "string") - throw new Error("db.root_key must be a string"); - const value = decryptAuthSecret( - envOverride("SUPABASE_DB_ROOT_KEY", raw, context.projectEnvValues), - context.projectEnvValues, - ); - return value === undefined || value === "" ? undefined : value; - }, - catch: (cause) => new StackConfigError({ message: String(cause) }), + const rootKey = yield* Effect.gen(function* () { + const raw = section(document, "db")?.root_key; + if (raw !== undefined && typeof raw !== "string") + return yield* new StackConfigError({ message: "Error: db.root_key must be a string" }); + const value = yield* decryptAuthSecret( + yield* envOverride("SUPABASE_DB_ROOT_KEY", raw, context.projectEnvValues), + context.projectEnvValues, + ).pipe(Effect.mapError((cause) => new StackConfigError({ message: String(cause) }))); + return value === undefined || value === "" ? undefined : value; }); - const dbPort = resolvePort( + const dbPort = yield* resolvePort( DB_PORT, document, validatedConfig.db.port, context.projectEnvValues, ); - const apiPort = resolvePort( + const apiPort = yield* resolvePort( API_PORT, document, validatedConfig.api.port, context.projectEnvValues, ); - const studioPort = resolvePort( + const studioPort = yield* resolvePort( STUDIO_PORT, document, validatedConfig.studio.port, context.projectEnvValues, ); - const poolerPort = resolvePort( + const poolerPort = yield* resolvePort( DB_POOLER_PORT, document, validatedConfig.db.pooler.port, context.projectEnvValues, ); - const mailPort = resolvePort( + const mailPort = yield* resolvePort( LOCAL_SMTP_PORT, document, validatedConfig.local_smtp.port, context.projectEnvValues, ); - const mailSmtpPort = resolvePort( + const mailSmtpPort = yield* resolvePort( LOCAL_SMTP_SMTP_PORT, document, validatedConfig.local_smtp.smtp_port ?? 0, context.projectEnvValues, ); - const mailPop3Port = resolvePort( + const mailPop3Port = yield* resolvePort( LOCAL_SMTP_POP3_PORT, document, validatedConfig.local_smtp.pop3_port ?? 0, context.projectEnvValues, ); - const analyticsPort = resolvePort( + const analyticsPort = yield* resolvePort( ANALYTICS_PORT, document, validatedConfig.analytics.port, @@ -1267,7 +1308,7 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( const createCreations = ( stackId: string, ): Effect.Effect, StackConfigError> => - Effect.sync(() => { + Effect.gen(function* () { return [ { service: "database", @@ -1410,7 +1451,7 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( endpoints: { http: endpoint(apiPort), inspector: endpoint( - resolvePort( + yield* resolvePort( EDGE_RUNTIME_INSPECTOR_PORT, document, validatedConfig.edge_runtime.inspector_port, diff --git a/apps/cli/src/command-internal/status-values.ts b/apps/cli/src/command-internal/status-values.ts index ff8240a8cd..782624ab35 100644 --- a/apps/cli/src/command-internal/status-values.ts +++ b/apps/cli/src/command-internal/status-values.ts @@ -1,4 +1,5 @@ import type { CliConfig } from "@supabase/config"; +import { Effect } from "effect"; import { dockerfileServiceImageRaw } from "../shared/services/dockerfile-images.ts"; import { serviceContainerIds } from "./docker-ids.ts"; @@ -209,7 +210,7 @@ export interface StatusState { } /** - * The validating half of {@link StatusState}: everything that can throw, and none of it + * The validating half of {@link StatusState}: everything that can fail, and none of it * depends on `excluded`/`containerIds`. Split out so `status.handler.ts` can resolve and * validate this before any Docker call — a bad `auth.jwt_secret` or malformed * `SUPABASE_*_PORT`/`SUPABASE_*_ENABLED` override must fail here, not be masked by a @@ -228,21 +229,21 @@ export interface StatusLocalState { } /** - * Resolves local config values (URLs, keys — can throw, see {@link resolveLocalConfigValues}) + * Resolves local config values (URLs, keys — can fail, see {@link resolveLocalConfigValues}) * and the per-service `.enabled` gates, with no reference to `excluded`/`containerIds` — see * {@link gateStatusState} for the Docker-dependent half this composes with. Each `.enabled` * gate is read through {@link envOverrideBool}, not the raw decoded `config.
.enabled`, * so an env-overridden stack's running services match what `status` reports. * - * @throws {InvalidJwtSecretError} when `auth.jwt_secret` is set but too short. - * @throws {InvalidPortEnvOverrideError} when a `SUPABASE_*_PORT` env/dotenv override doesn't - * parse as a valid port. - * @throws {InvalidBoolEnvOverrideError} when a `SUPABASE_*_ENABLED` env/dotenv override - * doesn't parse as a valid bool. - * @throws when `auth.signing_keys_path` is set but the file is missing, malformed, or its + * Fails with {@link InvalidJwtSecretError} when `auth.jwt_secret` is set but too short. + * Fails with {@link InvalidPortEnvOverrideError} when a `SUPABASE_*_PORT` env/dotenv + * override doesn't parse as a valid port. + * Fails with {@link InvalidBoolEnvOverrideError} when a `SUPABASE_*_ENABLED` env/dotenv + * override doesn't parse as a valid bool. + * Fails when `auth.signing_keys_path` is set but the file is missing, malformed, or its * first key is unsupported — see {@link generateAsymmetricGoJwt}. */ -export function resolveStatusLocalState( +export const resolveStatusLocalState = Effect.fnUntraced(function* ( config: CliConfig, hostname: string, workdir: string, @@ -258,48 +259,48 @@ export function resolveStatusLocalState( * containers. */ precomputedLocal?: LocalConfigValues, -): StatusLocalState { +) { const local = precomputedLocal ?? - resolveLocalConfigValues(config, hostname, workdir, projectEnvValues, document); + (yield* resolveLocalConfigValues(config, hostname, workdir, projectEnvValues, document)); - const apiEnabled = envOverrideBool( + const apiEnabled = yield* envOverrideBool( "SUPABASE_API_ENABLED", config.api.enabled, "api.enabled", projectEnvValues, ); - const studioSectionEnabled = envOverrideBool( + const studioSectionEnabled = yield* envOverrideBool( "SUPABASE_STUDIO_ENABLED", config.studio.enabled, "studio.enabled", projectEnvValues, ); - const authSectionEnabled = envOverrideBool( + const authSectionEnabled = yield* envOverrideBool( "SUPABASE_AUTH_ENABLED", config.auth.enabled, "auth.enabled", projectEnvValues, ); - const inbucketSectionEnabled = envOverrideBool( + const inbucketSectionEnabled = yield* envOverrideBool( "SUPABASE_LOCAL_SMTP_ENABLED", config.local_smtp.enabled, "local_smtp.enabled", projectEnvValues, ); - const storageSectionEnabled = envOverrideBool( + const storageSectionEnabled = yield* envOverrideBool( "SUPABASE_STORAGE_ENABLED", config.storage.enabled, "storage.enabled", projectEnvValues, ); - const edgeRuntimeEnabled = envOverrideBool( + const edgeRuntimeEnabled = yield* envOverrideBool( "SUPABASE_EDGE_RUNTIME_ENABLED", config.edge_runtime.enabled, "edge_runtime.enabled", projectEnvValues, ); - const storageS3ProtocolEnabled = envOverrideBool( + const storageS3ProtocolEnabled = yield* envOverrideBool( "SUPABASE_STORAGE_S3_PROTOCOL_ENABLED", config.storage.s3_protocol.enabled, "storage.s3_protocol.enabled", @@ -316,14 +317,14 @@ export function resolveStatusLocalState( storageSectionEnabled, edgeRuntimeEnabled, storageS3ProtocolEnabled, - }; -} + } satisfies StatusLocalState; +}); /** * The Docker-dependent, non-throwing half of status resolution: applies `excluded`, matching * each gated service by its container id ({@link statusContainerIds}) or its default Docker * image short name ({@link shortContainerImageName}) — service images are never - * user-overridable, so the default is always the one to check. Pure: every throwing concern + * user-overridable, so the default is always the one to check. Pure: every fallible concern * already ran in {@link resolveStatusLocalState}. */ export function gateStatusState( @@ -439,7 +440,7 @@ export function deprecatedStatusFieldWarning( * map. `status.handler.ts` calls the three separately so it can validate before any Docker * call and reuse the gated state for both the real and pretty-mode value maps. */ -export function statusValues( +export const statusValues = Effect.fnUntraced(function* ( config: CliConfig, containerIds: StatusContainerIds, hostname: string, @@ -449,8 +450,14 @@ export function statusValues( projectEnvValues?: Readonly>, /** `LoadedCliConfig.document` — see {@link resolveLocalConfigValues}'s doc comment. */ document?: Readonly>, -): StatusValuesResult { - const localState = resolveStatusLocalState(config, hostname, workdir, projectEnvValues, document); +) { + const localState = yield* resolveStatusLocalState( + config, + hostname, + workdir, + projectEnvValues, + document, + ); const state = gateStatusState(localState, containerIds, excluded); return statusValuesFromState(state, overrides); -} +}); diff --git a/apps/cli/src/command-internal/status-values.unit.test.ts b/apps/cli/src/command-internal/status-values.unit.test.ts index 2ef704e793..f09b8ca268 100644 --- a/apps/cli/src/command-internal/status-values.unit.test.ts +++ b/apps/cli/src/command-internal/status-values.unit.test.ts @@ -1,6 +1,7 @@ +import { BunServices } from "@effect/platform-bun"; import { afterEach, describe, expect, it, vi } from "@effect/vitest"; import { CliConfigSchema, type CliConfig } from "@supabase/config"; -import { Effect, Schema } from "effect"; +import { Effect, FileSystem, Path, Schema } from "effect"; import { shortContainerImageName, @@ -30,6 +31,9 @@ const NONE: ReadonlyArray = []; const NO_OVERRIDES = new Map(); const WORKDIR = "/tmp/status-values-test"; +const run = (effect: Effect.Effect) => + Effect.runSync(effect.pipe(Effect.provide(BunServices.layer))); + describe("statusValues", () => { it("emits DB_URL unconditionally, even when every other service is disabled/excluded", () => { const config = baseConfig({ @@ -40,13 +44,8 @@ describe("statusValues", () => { storage: { enabled: false }, edge_runtime: { enabled: false }, }); - const { values } = statusValues( - config, - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.kong], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, [CONTAINER_IDS.kong], NO_OVERRIDES, WORKDIR), ); expect(Object.keys(values)).toEqual(["DB_URL"]); expect(values.DB_URL).toContain("postgresql://postgres:postgres@127.0.0.1"); @@ -54,20 +53,17 @@ describe("statusValues", () => { describe("api / kong gating", () => { it("includes API_URL when api.enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.API_URL).toBeDefined(); }); it("keeps the Kong URLs but omits REST/GraphQL when api.enabled is false", () => { const config = baseConfig({ api: { enabled: false } }); - const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR); + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), + ); expect(values.API_URL).toBeDefined(); expect(values.FUNCTIONS_URL).toBeDefined(); expect(values.MCP_URL).toBeDefined(); @@ -76,50 +72,51 @@ describe("statusValues", () => { }); it("omits API_URL when the kong container id is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.kong], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.kong], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.API_URL).toBeUndefined(); }); it("omits API_URL when the kong image short name is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - ["kong"], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, ["kong"], NO_OVERRIDES, WORKDIR), ); expect(values.API_URL).toBeUndefined(); }); it("omits REST/GraphQL when kong is disabled even though postgrest is enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.kong], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.kong], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.REST_URL).toBeUndefined(); expect(values.GRAPHQL_URL).toBeUndefined(); }); it("omits REST/GraphQL when only the rest container id is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.rest], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.rest], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.API_URL).toBeDefined(); expect(values.REST_URL).toBeUndefined(); @@ -127,26 +124,16 @@ describe("statusValues", () => { }); it("includes REST/GraphQL when kong and postgrest are both enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.REST_URL).toBeDefined(); expect(values.GRAPHQL_URL).toBeDefined(); }); it("omits REST/GraphQL when the postgrest image short name is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - ["postgrest"], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, ["postgrest"], NO_OVERRIDES, WORKDIR), ); expect(values.API_URL).toBeDefined(); expect(values.REST_URL).toBeUndefined(); @@ -156,43 +143,44 @@ describe("statusValues", () => { describe("functions gating", () => { it("includes FUNCTIONS_URL when kong and edge_runtime are both enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.FUNCTIONS_URL).toBeDefined(); }); it("omits FUNCTIONS_URL when edge_runtime.enabled is false", () => { const config = baseConfig({ edge_runtime: { enabled: false } }); - const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR); + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), + ); expect(values.FUNCTIONS_URL).toBeUndefined(); }); it("omits FUNCTIONS_URL when kong is disabled even though edge_runtime is enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.kong], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.kong], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.FUNCTIONS_URL).toBeUndefined(); }); it("omits FUNCTIONS_URL when the edge_runtime container id is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.edgeRuntime], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.edgeRuntime], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.FUNCTIONS_URL).toBeUndefined(); }); @@ -200,13 +188,15 @@ describe("statusValues", () => { it("omits FUNCTIONS_URL when the edge-runtime image short name is excluded", () => { // The image repo name (`supabase/edge-runtime`) differs from the Dockerfile build // alias (`edgeruntime`); the short name matched against is the former. - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - ["edge-runtime"], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + ["edge-runtime"], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.FUNCTIONS_URL).toBeUndefined(); }); @@ -214,87 +204,75 @@ describe("statusValues", () => { describe("studio / mcp gating", () => { it("includes STUDIO_URL when studio.enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.STUDIO_URL).toBeDefined(); }); it("omits STUDIO_URL when studio.enabled is false", () => { const config = baseConfig({ studio: { enabled: false } }); - const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR); + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), + ); expect(values.STUDIO_URL).toBeUndefined(); }); it("omits STUDIO_URL when the studio container id is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.studio], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.studio], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.STUDIO_URL).toBeUndefined(); }); it("omits STUDIO_URL when the studio image short name is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - ["studio"], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, ["studio"], NO_OVERRIDES, WORKDIR), ); expect(values.STUDIO_URL).toBeUndefined(); }); it("includes MCP_URL only when both kong and studio are enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.MCP_URL).toBeDefined(); }); it("omits MCP_URL when kong is disabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.kong], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.kong], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.MCP_URL).toBeUndefined(); }); it("omits MCP_URL when studio is disabled", () => { const config = baseConfig({ studio: { enabled: false } }); - const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR); + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), + ); expect(values.MCP_URL).toBeUndefined(); }); }); describe("auth gating", () => { it("includes all 5 auth fields when auth.enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.PUBLISHABLE_KEY).toBeDefined(); expect(values.SECRET_KEY).toBeDefined(); @@ -305,7 +283,9 @@ describe("statusValues", () => { it("omits all 5 auth fields when auth.enabled is false", () => { const config = baseConfig({ auth: { enabled: false } }); - const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR); + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), + ); expect(values.PUBLISHABLE_KEY).toBeUndefined(); expect(values.SECRET_KEY).toBeUndefined(); expect(values.JWT_SECRET).toBeUndefined(); @@ -314,25 +294,22 @@ describe("statusValues", () => { }); it("omits all 5 auth fields when the auth container id is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.auth], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.auth], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.PUBLISHABLE_KEY).toBeUndefined(); }); it("omits all 5 auth fields when the gotrue image short name is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - ["gotrue"], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, ["gotrue"], NO_OVERRIDES, WORKDIR), ); expect(values.PUBLISHABLE_KEY).toBeUndefined(); }); @@ -340,13 +317,8 @@ describe("statusValues", () => { describe("inbucket/mailpit gating", () => { it("includes MAILPIT_URL and the deprecated INBUCKET_URL alias when local_smtp.enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.MAILPIT_URL).toBeDefined(); expect(values.INBUCKET_URL).toBe(values.MAILPIT_URL); @@ -354,31 +326,30 @@ describe("statusValues", () => { it("omits MAILPIT_URL/INBUCKET_URL when local_smtp.enabled is false", () => { const config = baseConfig({ local_smtp: { enabled: false } }); - const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR); + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), + ); expect(values.MAILPIT_URL).toBeUndefined(); expect(values.INBUCKET_URL).toBeUndefined(); }); it("omits MAILPIT_URL/INBUCKET_URL when the inbucket container id is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.inbucket], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.inbucket], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.MAILPIT_URL).toBeUndefined(); }); it("omits MAILPIT_URL/INBUCKET_URL when the mailpit image short name is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - ["mailpit"], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, ["mailpit"], NO_OVERRIDES, WORKDIR), ); expect(values.MAILPIT_URL).toBeUndefined(); }); @@ -386,13 +357,8 @@ describe("statusValues", () => { describe("storage / s3 gating", () => { it("includes all 4 storage S3 fields when storage.enabled and s3_protocol.enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), ); expect(values.STORAGE_S3_URL).toBeDefined(); expect(values.S3_PROTOCOL_ACCESS_KEY_ID).toBeDefined(); @@ -402,18 +368,22 @@ describe("statusValues", () => { it("omits storage S3 fields when storage.enabled is false", () => { const config = baseConfig({ storage: { enabled: false } }); - const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR); + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), + ); expect(values.STORAGE_S3_URL).toBeUndefined(); }); it("omits storage S3 fields when the storage container id is excluded", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [CONTAINER_IDS.storage], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues( + baseConfig(), + CONTAINER_IDS, + HOSTNAME, + [CONTAINER_IDS.storage], + NO_OVERRIDES, + WORKDIR, + ), ); expect(values.STORAGE_S3_URL).toBeUndefined(); }); @@ -421,20 +391,17 @@ describe("statusValues", () => { it("omits storage S3 fields when the storage-api image short name is excluded", () => { // The image repo name (`supabase/storage-api`) differs from the Dockerfile build // alias (`storage`); the short name matched against is the former. - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - ["storage-api"], - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, ["storage-api"], NO_OVERRIDES, WORKDIR), ); expect(values.STORAGE_S3_URL).toBeUndefined(); }); it("omits storage S3 fields when storage.s3_protocol.enabled is false", () => { const config = baseConfig({ storage: { s3_protocol: { enabled: false } } }); - const { values } = statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR); + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR), + ); expect(values.STORAGE_S3_URL).toBeUndefined(); expect(values.S3_PROTOCOL_ACCESS_KEY_ID).toBeUndefined(); }); @@ -446,30 +413,20 @@ describe("statusValues", () => { it("includes API_URL/REST_URL when SUPABASE_API_ENABLED overrides a disabled api.enabled", () => { const config = baseConfig({ api: { enabled: false } }); - const { values } = statusValues( - config, - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { SUPABASE_API_ENABLED: "true", - }, + }), ); expect(values.API_URL).toBeDefined(); expect(values.REST_URL).toBeDefined(); }); it("omits REST_URL when SUPABASE_API_ENABLED=false overrides an enabled api.enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { SUPABASE_API_ENABLED: "false" }, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { + SUPABASE_API_ENABLED: "false", + }), ); expect(values.REST_URL).toBeUndefined(); expect(values.API_URL).toBeDefined(); @@ -477,32 +434,20 @@ describe("statusValues", () => { it("includes STUDIO_URL when SUPABASE_STUDIO_ENABLED overrides a disabled studio.enabled", () => { const config = baseConfig({ studio: { enabled: false } }); - const { values } = statusValues( - config, - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { SUPABASE_STUDIO_ENABLED: "true", - }, + }), ); expect(values.STUDIO_URL).toBeDefined(); }); it("includes the 5 auth fields when SUPABASE_AUTH_ENABLED overrides a disabled auth.enabled", () => { const config = baseConfig({ auth: { enabled: false } }); - const { values } = statusValues( - config, - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { SUPABASE_AUTH_ENABLED: "true", - }, + }), ); expect(values.PUBLISHABLE_KEY).toBeDefined(); expect(values.ANON_KEY).toBeDefined(); @@ -510,91 +455,59 @@ describe("statusValues", () => { }); it("omits the 5 auth fields when SUPABASE_AUTH_ENABLED=false overrides an enabled auth.enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { SUPABASE_AUTH_ENABLED: "false" }, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { + SUPABASE_AUTH_ENABLED: "false", + }), ); expect(values.PUBLISHABLE_KEY).toBeUndefined(); }); it("includes MAILPIT_URL when SUPABASE_LOCAL_SMTP_ENABLED overrides a disabled local_smtp.enabled", () => { const config = baseConfig({ local_smtp: { enabled: false } }); - const { values } = statusValues( - config, - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { SUPABASE_LOCAL_SMTP_ENABLED: "true", - }, + }), ); expect(values.MAILPIT_URL).toBeDefined(); }); it("includes storage S3 fields when SUPABASE_STORAGE_ENABLED overrides a disabled storage.enabled", () => { const config = baseConfig({ storage: { enabled: false } }); - const { values } = statusValues( - config, - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { SUPABASE_STORAGE_ENABLED: "true", - }, + }), ); expect(values.STORAGE_S3_URL).toBeDefined(); }); it("includes FUNCTIONS_URL when SUPABASE_EDGE_RUNTIME_ENABLED overrides a disabled edge_runtime.enabled", () => { const config = baseConfig({ edge_runtime: { enabled: false } }); - const { values } = statusValues( - config, - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { SUPABASE_EDGE_RUNTIME_ENABLED: "true", - }, + }), ); expect(values.FUNCTIONS_URL).toBeDefined(); }); it("includes storage S3 fields when SUPABASE_STORAGE_S3_PROTOCOL_ENABLED overrides a disabled s3_protocol.enabled", () => { const config = baseConfig({ storage: { s3_protocol: { enabled: false } } }); - const { values } = statusValues( - config, - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { + const { values } = run( + statusValues(config, CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { SUPABASE_STORAGE_S3_PROTOCOL_ENABLED: "true", - }, + }), ); expect(values.STORAGE_S3_URL).toBeDefined(); }); it("omits storage S3 fields when SUPABASE_STORAGE_S3_PROTOCOL_ENABLED=false overrides an enabled s3_protocol.enabled", () => { - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - NO_OVERRIDES, - WORKDIR, - { SUPABASE_STORAGE_S3_PROTOCOL_ENABLED: "false" }, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, NO_OVERRIDES, WORKDIR, { + SUPABASE_STORAGE_S3_PROTOCOL_ENABLED: "false", + }), ); expect(values.STORAGE_S3_URL).toBeUndefined(); }); @@ -603,13 +516,8 @@ describe("statusValues", () => { describe("--override-name remapping", () => { it("remaps a field's output KEY while leaving the value unchanged", () => { const overrides = new Map([["api.url", "NEXT_PUBLIC_SUPABASE_URL"]]); - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - overrides, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, overrides, WORKDIR), ); expect(values.API_URL).toBeUndefined(); expect(values.NEXT_PUBLIC_SUPABASE_URL).toBe("http://127.0.0.1:54321"); @@ -620,13 +528,8 @@ describe("statusValues", () => { ["api.url", "CUSTOM_API_URL"], ["db.url", "CUSTOM_DB_URL"], ]); - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - overrides, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, overrides, WORKDIR), ); expect(values.CUSTOM_API_URL).toBeDefined(); expect(values.CUSTOM_DB_URL).toBeDefined(); @@ -636,13 +539,8 @@ describe("statusValues", () => { it("leaves unrelated fields at their default name when only one is overridden", () => { const overrides = new Map([["api.url", "CUSTOM_API_URL"]]); - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - overrides, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, overrides, WORKDIR), ); expect(values.REST_URL).toBeDefined(); }); @@ -653,13 +551,8 @@ describe("statusValues", () => { ["auth.anon_key", "CUSTOM_ANON_KEY"], ["auth.service_role_key", "CUSTOM_SERVICE_ROLE_KEY"], ]); - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - overrides, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, overrides, WORKDIR), ); expect(values.CUSTOM_JWT_SECRET).toBeDefined(); expect(values.CUSTOM_ANON_KEY).toBeDefined(); @@ -671,13 +564,8 @@ describe("statusValues", () => { it("remaps the deprecated inbucket.url key independently of mailpit.url", () => { const overrides = new Map([["inbucket.url", "CUSTOM_INBUCKET_URL"]]); - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - NONE, - overrides, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, NONE, overrides, WORKDIR), ); expect(values.CUSTOM_INBUCKET_URL).toBeDefined(); expect(values.MAILPIT_URL).toBeDefined(); @@ -688,13 +576,8 @@ describe("statusValues", () => { it("combines stopped-service exclusions with --exclude flag exclusions", () => { // Both `stopped` and `--exclude` funnel into the same `excluded` array in the handler. const excluded = [CONTAINER_IDS.storage, CONTAINER_IDS.studio]; - const { values } = statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - excluded, - NO_OVERRIDES, - WORKDIR, + const { values } = run( + statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, excluded, NO_OVERRIDES, WORKDIR), ); expect(values.STORAGE_S3_URL).toBeUndefined(); expect(values.STUDIO_URL).toBeUndefined(); @@ -726,14 +609,9 @@ describe("--exclude image short names under SUPABASE_USE_SLIM_IMAGES", () => { ["studio", "STUDIO_URL"], ["edge-runtime", "FUNCTIONS_URL"], ] as const) { - const { values } = slimModule.statusValues( - baseConfig(), - CONTAINER_IDS, - HOSTNAME, - [excluded], - NO_OVERRIDES, - WORKDIR, - ); + const { values } = yield* slimModule + .statusValues(baseConfig(), CONTAINER_IDS, HOSTNAME, [excluded], NO_OVERRIDES, WORKDIR) + .pipe(Effect.provide(BunServices.layer)); expect(values[omitted], `--exclude ${excluded}`).toBeUndefined(); } }), diff --git a/apps/cli/src/command-internal/storage-credentials.ts b/apps/cli/src/command-internal/storage-credentials.ts index 0b4158db5f..5c25c886fb 100644 --- a/apps/cli/src/command-internal/storage-credentials.ts +++ b/apps/cli/src/command-internal/storage-credentials.ts @@ -164,7 +164,7 @@ export const resolveStorageCredentials = Effect.fnUntraced(function* (opts: { }); /** - * Converts a thrown config-load validation error (from `envOverride*`, + * Converts a config-load validation error (from `envOverride*`, * `decryptAuthSecret`, `resolveJwtSecret`, `validateApi*`) into a tagged * `StorageConfigError`, preserving the original message. */ @@ -178,37 +178,44 @@ const toStorageConfigError = (cause: unknown) => * URL, so a stack started with an overridden port stays reachable here * instead of falling back to the raw `config.toml` value. */ -const resolveLocalApiConfig = ( +const resolveLocalApiConfig = Effect.fnUntraced(function* ( api: StorageConfigView["api"], projectEnvValues: Readonly>, -) => - Effect.try({ - try: () => { - const resolved = { - enabled: envOverrideBool( - "SUPABASE_API_ENABLED", - api.enabled, - "api.enabled", - projectEnvValues, - ), - external_url: envOverride("SUPABASE_API_EXTERNAL_URL", api.external_url, projectEnvValues), - port: envOverridePort("SUPABASE_API_PORT", api.port, "api.port", projectEnvValues), - tls: { - enabled: envOverrideBool( - "SUPABASE_API_TLS_ENABLED", - api.tls.enabled, - "api.tls.enabled", - projectEnvValues, - ), - cert_path: envOverride("SUPABASE_API_TLS_CERT_PATH", api.tls.cert_path, projectEnvValues), - key_path: envOverride("SUPABASE_API_TLS_KEY_PATH", api.tls.key_path, projectEnvValues), - }, - } satisfies StorageConfigView["api"]; - validateApiPort(resolved.enabled, resolved.port); - return resolved; +) { + const resolved = { + enabled: yield* envOverrideBool( + "SUPABASE_API_ENABLED", + api.enabled, + "api.enabled", + projectEnvValues, + ), + external_url: yield* envOverride( + "SUPABASE_API_EXTERNAL_URL", + api.external_url, + projectEnvValues, + ), + port: yield* envOverridePort("SUPABASE_API_PORT", api.port, "api.port", projectEnvValues), + tls: { + enabled: yield* envOverrideBool( + "SUPABASE_API_TLS_ENABLED", + api.tls.enabled, + "api.tls.enabled", + projectEnvValues, + ), + cert_path: yield* envOverride( + "SUPABASE_API_TLS_CERT_PATH", + api.tls.cert_path, + projectEnvValues, + ), + key_path: yield* envOverride("SUPABASE_API_TLS_KEY_PATH", api.tls.key_path, projectEnvValues), }, + } satisfies StorageConfigView["api"]; + yield* Effect.try({ + try: () => validateApiPort(resolved.enabled, resolved.port), catch: toStorageConfigError, }); + return resolved; +}, Effect.mapError(toStorageConfigError)); /** * Resolves the service-role key for the local Storage gateway: @@ -223,24 +230,14 @@ const resolveLocalServiceRoleKey = Effect.fnUntraced(function* ( auth: StorageConfigView["auth"], projectEnvValues: Readonly>, ) { - const jwtSecret = yield* Effect.try({ - try: () => - resolveJwtSecret( - decryptAuthSecret( - envOverride("SUPABASE_AUTH_JWT_SECRET", auth.jwt_secret, projectEnvValues), - projectEnvValues, - ), - ), - catch: toStorageConfigError, - }); - const configuredKey = yield* Effect.try({ - try: () => - decryptAuthSecret( - envOverride("SUPABASE_AUTH_SERVICE_ROLE_KEY", auth.service_role_key, projectEnvValues), - projectEnvValues, - ), - catch: toStorageConfigError, - }); + const jwtSecret = yield* decryptAuthSecret( + yield* envOverride("SUPABASE_AUTH_JWT_SECRET", auth.jwt_secret, projectEnvValues), + projectEnvValues, + ).pipe(Effect.flatMap(resolveJwtSecret), Effect.mapError(toStorageConfigError)); + const configuredKey = yield* decryptAuthSecret( + yield* envOverride("SUPABASE_AUTH_SERVICE_ROLE_KEY", auth.service_role_key, projectEnvValues), + projectEnvValues, + ).pipe(Effect.mapError(toStorageConfigError)); return configuredKey !== undefined && configuredKey.length > 0 ? configuredKey : generateGoJwt(jwtSecret, "service_role"); diff --git a/apps/cli/src/commands/config/push/SIDE_EFFECTS.md b/apps/cli/src/commands/config/push/SIDE_EFFECTS.md index 30424ddcbd..f88703be93 100644 --- a/apps/cli/src/commands/config/push/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/config/push/SIDE_EFFECTS.md @@ -361,7 +361,7 @@ may itself contain a `.`. - **A non-TTY script piping multiple `y`/`n` answers needs one extra leading answer for an IMPLICIT branch target.** The branch confirmation gate reads one piped stdin line just like any other prompt in this command; it runs before the per-service `keep()` prompts, so a script written for the pre-CLI-2168 prompt sequence (`api`, `db`, `auth`, ...) has every answer shifted by one when its target happens to be an inferred branch. A plain-project target, or a target named explicitly via `--project-ref`, is unaffected (no new prompt fires). - The post-run linked-project telemetry cache fill (`Effect.ensuring`, unconditional) may issue its own `GET /v1/projects/{ref}` independent of the target-detection probe above — both are best-effort/non-fatal for that fill, so a branch ref 404ing there is expected and harmless. - Run from the project root (or pass `--workdir`); `config.toml` is read relative to it. -- Auth email `content_path` resolution: `[auth.email.template.*]` and `[auth.email.notification.*]` paths are relative to the discovered project root; notification paths fall back to the legacy `supabase/`-relative location when the root-resolved file is missing. Notification HTML is read only when `enabled = true`. **Every resolved path — relative (after collapsing `..`), absolute, or reached through an in-root symlink — must stay inside the project root** (CLI-2320; symlinks are dereferenced with `realpathSync` before the check, so an in-root symlink pointing outside can't bypass it); a path that resolves outside it aborts with `Invalid config for auth.email...content_path: resolves outside the project root ()`, before any file read. This containment check now lives centrally in `resolveEmailTemplateContentPath` (`config-validate.ts`) rather than in this command's own module (CLI-2339), and applies unconditionally to every caller of that resolver — config validation and `start`'s eager pre-Docker pass, not just `config push`. +- Auth email `content_path` resolution: `[auth.email.template.*]` and `[auth.email.notification.*]` paths are relative to the discovered project root; notification paths fall back to the legacy `supabase/`-relative location when the root-resolved file is missing. Notification HTML is read only when `enabled = true`. **Every resolved path — relative (after collapsing `..`), absolute, or reached through an in-root symlink — must stay inside the project root** (CLI-2320; symlinks are dereferenced using `fs.realPath` before the check, so an in-root symlink pointing outside can't bypass it); a path that resolves outside it aborts with `Invalid config for auth.email...content_path: resolves outside the project root ()`, before any file read. This containment check now lives centrally in `resolveEmailTemplateContentPath` (`config-validate.ts`) rather than in this command's own module (CLI-2339), and applies unconditionally to every caller of that resolver — config validation and `start`'s eager pre-Docker pass, not just `config push`. - **Only properties your file declares, and whose value differs from the project, are written.** Fields the API requires together ship as a group; undeclared members of that group are sent with the project's CURRENT value, read in the same run — so they do not change. Only when the read did not return a member's current value is it sent at the config schema default, and that is always disclosed (a `[group-write]` block in the confirmation output, a `forced` entry in the JSON payload, and a summary `Note:` line) — never applied silently. - **`db.ssl_enforcement`'s presence, not its decoded default, decides the gate.** `@supabase/config`'s projection recovers whether `[db.ssl_enforcement]` (and `storage.image_transformation`/`storage.s3_protocol`) were actually declared, as opposed to decoding to a schema default; an undeclared `[db.ssl_enforcement]` is treated as `disabled` — no read is needed for this any more, since row 1's single response already carries the remote value. - Optional `*pointer` sections (`db.ssl_enforcement`, `storage.image_transformation`, `storage.s3_protocol`) follow that same presence rule end to end — declared-but-absent is never confused with explicitly-disabled. diff --git a/apps/cli/src/commands/config/push/push.auth-email-content.ts b/apps/cli/src/commands/config/push/push.auth-email-content.ts index 60cf929f00..534a13ad03 100644 --- a/apps/cli/src/commands/config/push/push.auth-email-content.ts +++ b/apps/cli/src/commands/config/push/push.auth-email-content.ts @@ -9,7 +9,9 @@ import type { CliConfig } from "@supabase/config"; import { Effect, FileSystem } from "effect"; import { + ConfigValidateError, emailContentPathReadErrorMessage, + fsErrorCause, resolveEmailTemplateContentPath, } from "../../../command-internal/config-validate.ts"; import { ConfigPushLoadConfigError } from "./push.errors.ts"; @@ -62,16 +64,15 @@ const readTemplateContent = Effect.fnUntraced(function* ( return new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes); }); -const resolveContentPath = ( - options: Parameters[0], -): Effect.Effect => - Effect.try({ - try: () => resolveEmailTemplateContentPath(options), - catch: (cause) => - new ConfigPushLoadConfigError({ +const resolveContentPath = (options: Parameters[0]) => + resolveEmailTemplateContentPath(options).pipe( + Effect.mapError((error) => { + const cause = error instanceof ConfigValidateError ? error : fsErrorCause(error); + return new ConfigPushLoadConfigError({ message: cause instanceof Error ? cause.message : String(cause), - }), - }); + }); + }), + ); /** * Loads auth email template HTML from disk for `config push`. diff --git a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md index 679781b858..58bd4dc5dd 100644 --- a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md @@ -32,7 +32,7 @@ it, and JSON `null` disables formatting without disabling safe compaction. | `/supabase/config.toml` | TOML | always (db port/password, `[experimental.pgdelta]`, deno_version) | | `/supabase/.env`, `.env.local`, project-root/`SUPABASE_ENV`-selected dotenv file | dotenv | shadow provisioning (all native targets, including the explicit `--from/--to migrations` shadow) | | `api.tls.cert_path` / `api.tls.key_path` (under `/supabase/`) | PEM | shadow provisioning, when `api.enabled && api.tls.enabled` | -| `auth.email.template.*` / `auth.email.notification.*` `content_path` (config-relative or absolute) | text (existence/readability only — bytes discarded, used only to validate the config) | only when `auth.enabled`, for every configured template and every notification with `enabled = true` — via the same `readDbToml`/`checkDbToml` `Config.Validate` pipeline shared by every `db`/`migration` subcommand that loads config (`db dump`/`pull`/`reset`/`push`/`schema declarative generate`/`sync`, `migration up`/`down`/`squash` — documented once here rather than duplicated per file, CLI-2339); the resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) — a path resolving outside it aborts before the read | +| `auth.email.template.*` / `auth.email.notification.*` `content_path` (config-relative or absolute) | text (existence/readability only — bytes discarded, used only to validate the config) | only when `auth.enabled`, for every configured template and every notification with `enabled = true` — via the same `readDbToml`/`checkDbToml` `Config.Validate` pipeline shared by every `db`/`migration` subcommand that loads config (`db dump`/`pull`/`reset`/`push`/`schema declarative generate`/`sync`, `migration up`/`down`/`squash` — documented once here rather than duplicated per file, CLI-2339); the resolved path is CONFINED to the project root (symlinks dereferenced using `fs.realPath`) — a path resolving outside it aborts before the read | | `/supabase/migrations/*.sql` | SQL | shadow provisioning (applied to the shadow source) — `--use-pgadmin` too, via the SAME `migrateShadowDatabase` | | `/supabase/roles.sql` | SQL | shadow provisioning, PG14 and PG15 alike (unlike `db reset`'s PG15-only local path); also hashed into the shadow-baseline cache key on every cache-eligible acquire, warm hits included (where no baseline is applied at all); missing file tolerated | | `~/.supabase/cache/shadow-baseline/shadow-baseline-.tar` | tar | warm shadow-cache hit — the matching snapshot is streamed into the fresh shadow; every cache-eligible acquire (warm hit and successful cold export) also enumerates and `stat`s every `shadow-baseline-*.tar` for LRU keep-3 + 2-day mtime TTL and may delete other keys (`SUPABASE_HOME` overrides the `~/.supabase` root) | diff --git a/apps/cli/src/commands/experimental/stack/start/start.handler.ts b/apps/cli/src/commands/experimental/stack/start/start.handler.ts index 70e1c19057..920b47f2b6 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.handler.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.handler.ts @@ -281,13 +281,16 @@ const formatConfigPath = (path: string): string => { }; /** The display key for a setting: its env var when that's what overrides it, else its config key. */ -const settingKeyLabel = ( +const settingKeyLabel = Effect.fnUntraced(function* ( setting: StackEndpointSetting, projectEnvValues: Readonly>, -): string => - [setting.envVar, setting.deprecatedEnvVar].find( - (name) => name !== undefined && envOverride(name, undefined, projectEnvValues) !== undefined, - ) ?? formatConfigPath(setting.configPath); +) { + for (const name of [setting.envVar, setting.deprecatedEnvVar]) { + if (name !== undefined && (yield* envOverride(name, undefined, projectEnvValues)) !== undefined) + return name; + } + return formatConfigPath(setting.configPath); +}); /** * One setting behind an incompatible path, reported as the JSON/stream-json error envelope's @@ -303,14 +306,14 @@ interface StructuredSettingChange { readonly editable: boolean; } -const describeSettingChange = ( +const describeSettingChange = Effect.fnUntraced(function* ( service: PlannedInstance["service"], path: string, savedCreation: ServiceCreation | undefined, requestedCreation: ServiceCreationInput | undefined, projectEnvValues: Readonly>, databaseFromServices: boolean, -): StructuredSettingChange | ReadonlyArray => { +): Effect.fn.Return> { if (service === "database" && path === "pin") { const savedVersion = databaseVersionOf(savedCreation); const requestedVersion = databaseVersionOf(requestedCreation); @@ -334,7 +337,7 @@ const describeSettingChange = ( changes.push({ service, path, - key: settingKeyLabel(stackMajorVersionSetting, projectEnvValues), + key: yield* settingKeyLabel(stackMajorVersionSetting, projectEnvValues), saved: savedMajor, requested: requestedMajor, editable: true, @@ -343,7 +346,7 @@ const describeSettingChange = ( changes.push({ service, path, - key: settingKeyLabel(stackOrioledbVersionSetting, projectEnvValues), + key: yield* settingKeyLabel(stackOrioledbVersionSetting, projectEnvValues), saved: savedOrioledb, requested: requestedOrioledb, // A catalog upgrade can retire the saved build, which the config then rejects. @@ -358,7 +361,7 @@ const describeSettingChange = ( return { service, path, - key: settingKeyLabel(setting, projectEnvValues), + key: yield* settingKeyLabel(setting, projectEnvValues), saved: endpointPortLabel(savedCreation?.endpoints, endpointName), requested: endpointPortLabel(requestedCreation?.endpoints, endpointName), editable: true, @@ -373,7 +376,7 @@ const describeSettingChange = ( requested: "changed", editable: false, }; -}; +}); /** Whether `[local.services]` names a published database release, which `[db]` settings no longer select. */ const databasePublishedBy = (source: CliConfig): boolean => { @@ -383,30 +386,30 @@ const databasePublishedBy = (source: CliConfig): boolean => { }; /** Every incompatible path across every rejected saved member, as one structured list. */ -const incompatibleSettingChanges = ( +const incompatibleSettingChanges = Effect.fnUntraced(function* ( planned: ReadonlyArray, savedConfigById: ReadonlyMap, requested: ReadonlyArray, projectEnvValues: Readonly>, databaseFromServices: boolean, -): ReadonlyArray => - planned - .filter((entry) => entry.member && entry.change === "incompatible") - .flatMap((entry) => - // Narrowed by the filter above; `Extract` isn't inferred through `.filter`. - entry.change === "incompatible" - ? entry.paths.flatMap((path) => - describeSettingChange( - entry.service, - path, - savedConfigById.get(entry.id), - requested.find((creation) => creation.service === entry.service), - projectEnvValues, - databaseFromServices, - ), - ) - : [], - ); +) { + const changes: Array = []; + for (const entry of planned) { + if (!entry.member || entry.change !== "incompatible") continue; + for (const path of entry.paths) { + const described = yield* describeSettingChange( + entry.service, + path, + savedConfigById.get(entry.id), + requested.find((creation) => creation.service === entry.service), + projectEnvValues, + databaseFromServices, + ); + changes.push(...[described].flat()); + } + } + return changes; +}); /** One deduplicated text line per distinct setting change (shared API port lines collapse to one). */ const settingChangeLines = ( @@ -442,7 +445,7 @@ const revertAdvice = (editableKeys: ReadonlyArray): string | undefined = : "Revert the settings listed to their saved values"; /** Rejects every saved member whose endpoints or database line or major the request would change. */ -const incompatibleChange = ( +const incompatibleChange = Effect.fnUntraced(function* ( planned: ReadonlyArray, savedConfigById: ReadonlyMap, requested: ReadonlyArray, @@ -450,15 +453,16 @@ const incompatibleChange = ( databaseFromServices: boolean, stackIdentity: { readonly id: string; readonly name?: string }, ownerLive: boolean, -): +): Effect.fn.Return< | { readonly error: StackCommandStartError; readonly changes: ReadonlyArray; /** The destroy invocation, when recreating the stack is among the remedies. */ readonly command: string | undefined; } - | undefined => { - const changes = incompatibleSettingChanges( + | undefined +> { + const changes = yield* incompatibleSettingChanges( planned, savedConfigById, requested, @@ -497,7 +501,7 @@ const incompatibleChange = ( suggestion, }), }; -}; +}); const selectedCreations = ( creations: ReadonlyArray, @@ -926,7 +930,7 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags ...(target.name === undefined ? {} : { name: target.name }), }; const savedConfigById = new Map(currentStatuses.map(({ id, config: saved }) => [id, saved])); - const rejected = incompatibleChange( + const rejected = yield* incompatibleChange( planned, savedConfigById, requested, diff --git a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md index 556ee104b0..d26ceb6bb7 100644 --- a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md @@ -7,7 +7,7 @@ | `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | | `/supabase/{.env,.env.local,.env.,.env..local}` and the same four at the project root | dotenv | project dotenv (`resolveProjectEnvironmentValues`), merged into the `SUPABASE_*` overrides below and threaded into registry resolution | | `/supabase/config.toml` | TOML | to resolve function config, project id, and local Functions — via `goConfigCompat`'s `tomlOnly: true`/`search: false` (same resolver `start`/`stop`/`status` use), so `config.json` is never read here and no ancestor directory is searched past `` — this now (CLI-2285) applies to the functions manifest inference as well, so the two loads can never disagree about which project they resolve; also runs the full `Config.Validate` pipeline (`resolveLocalConfigValues`), so an invalid config fails up front even for fields this command never otherwise reads | -| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | as part of the `Config.Validate` pipeline above, unconditionally; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) before it is read, aborting with `resolves outside the project root` before any other work | +| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | as part of the `Config.Validate` pipeline above, unconditionally; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced using `fs.realPath`) before it is read, aborting with `resolves outside the project root` before any other work | | `/supabase/functions//index.ts` | TypeScript | function source to deploy | | `/supabase/functions/**/deno.json*` | JSON/JSONC | when resolving import maps | | imported modules | TypeScript | when walking local import graphs for deploy uploads/bundles | diff --git a/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md index e2d013c49c..028e79b7b5 100644 --- a/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md @@ -11,7 +11,7 @@ | `/supabase/.temp/edge-runtime-version` | plain text | Read by `resolveEdgeRuntimeVersionPin()` in the handler in legacy project-state mode only (stack mode takes the tag from the locked Functions pin), before the shared downloader chooses `--use-api` vs Docker — only affects the resolved edge-runtime image tag on the Docker-unbundle path | | `/supabase/{.env,.env.local,.env.,.env..local}` and the same four at the project root | dotenv | Docker-unbundle path only, before resolving config.toml — project dotenv (`resolveProjectEnvironmentValues`), merged into the `SUPABASE_*` overrides below and threaded into registry resolution | | `/supabase/config.toml` | TOML | Read unconditionally after resolving the project ref, before checking `--use-api`/`--use-docker` or whether Docker is running — resolves `edge_runtime.deno_version` and `project_id` (`loadCliConfig`) for the Docker-unbundle path. `goViperCompat`'s `tomlOnly: true` means `config.json` is never read here, unlike other `loadCliConfig` callers. A malformed config fails here even on the `--use-api` invocation. Also runs the full `Config.Validate` pipeline (`resolveLocalConfigValues`, same one `start`/`stop`/`status` already use) — an invalid config (bad `db.major_version`, malformed auth hook, etc.) now fails the Docker-unbundle path up front, even for fields this command never otherwise reads. | -| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | Docker-unbundle path only, as part of the `Config.Validate` pipeline above — read even though this command never uses their contents; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) before it is read, aborting with `resolves outside the project root` before any other work | +| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | Docker-unbundle path only, as part of the `Config.Validate` pipeline above — read even though this command never uses their contents; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced using `fs.realPath`) before it is read, aborting with `resolves outside the project root` before any other work | | `/telemetry.json` | JSON | when present, before post-run telemetry state is refreshed | ## Files Written diff --git a/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md index 54085b42ff..02270298a4 100644 --- a/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md @@ -6,7 +6,7 @@ | -------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/supabase/config.toml` | TOML | on every startup / restart when the project config exists | | `/supabase/{.env,.env.local,.env.,.env..local}` and the same four at the project root | dotenv | on every startup / restart, a SECOND, independent read from the `env()`-interpolation one below — project dotenv (`resolveProjectEnvironmentValues`) feeding the `SUPABASE_*` overrides (network-id, deno-version, registry) and the `Config.Validate` pipeline, same one `start`/`stop`/`status` already use | -| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | on every startup / restart, as part of the `Config.Validate` pipeline above, unconditionally — read even though `serve` doesn't otherwise use their contents; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) before it is read, aborting with `resolves outside the project root` before any other work | +| `/supabase/`, `[api.tls]` cert/key paths, email template `content_path` — when configured | varies | on every startup / restart, as part of the `Config.Validate` pipeline above, unconditionally — read even though `serve` doesn't otherwise use their contents; a `content_path` resolved path is CONFINED to the project root (symlinks dereferenced using `fs.realPath`) before it is read, aborting with `resolves outside the project root` before any other work | | `/supabase/.temp/edge-runtime-version` | plain text | when present, to override the bundled edge-runtime image tag | | `/supabase/functions/.env` | dotenv | when `--env-file` is unset and the fallback env file exists | | `/supabase/functions//.env` | dotenv | for each enabled Function when `--env-file` is unset; values override the shared fallback for that Function only | diff --git a/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md b/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md index 62df24b373..f5ac95e80b 100644 --- a/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md @@ -22,7 +22,7 @@ and uses an automatically assigned port; `db.shadow_port` applies only to the le | Path | Format | When | | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `/supabase/config.toml` | TOML | always, twice: `@supabase/config` for the shadow's own spec, `readDbToml` for shadow port/password/vault/baseline | -| `auth.email.template.*` / `auth.email.notification.*` `content_path` (config-relative or absolute) | text (existence/readability only — bytes discarded, used only to validate the config) | only when `auth.enabled`, for every configured template and every notification with `enabled = true` — via the same `readDbToml` `Config.Validate` pipeline shared by `migration up`/`down` and every `db` subcommand that loads config (`dump`/`pull`/`reset`/`diff`/`push`/`schema declarative generate`/`sync` — documented on `db diff`'s `SIDE_EFFECTS.md` and here, rather than duplicated per file, CLI-2339); the resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) — a path resolving outside it aborts before the read | +| `auth.email.template.*` / `auth.email.notification.*` `content_path` (config-relative or absolute) | text (existence/readability only — bytes discarded, used only to validate the config) | only when `auth.enabled`, for every configured template and every notification with `enabled = true` — via the same `readDbToml` `Config.Validate` pipeline shared by `migration up`/`down` and every `db` subcommand that loads config (`dump`/`pull`/`reset`/`diff`/`push`/`schema declarative generate`/`sync` — documented on `db diff`'s `SIDE_EFFECTS.md` and here, rather than duplicated per file, CLI-2339); the resolved path is CONFINED to the project root (symlinks dereferenced using `fs.realPath`) — a path resolving outside it aborts before the read | | `/supabase/migrations/` | directory | always | | `/supabase/migrations/_*.sql` | SQL | each migration up to the target, applied to the shadow; the target file's own final content is read by `--version`/baseline lookups | | `/supabase/roles.sql` | SQL | shadow `SetupDatabase` (custom-roles seed); missing file tolerated | diff --git a/apps/cli/src/commands/notebooks/notebooks.shared.ts b/apps/cli/src/commands/notebooks/notebooks.shared.ts index 49228c549f..3ea7a7d4fa 100644 --- a/apps/cli/src/commands/notebooks/notebooks.shared.ts +++ b/apps/cli/src/commands/notebooks/notebooks.shared.ts @@ -5,6 +5,7 @@ import { block } from "../../command-internal/command-permissions/model.ts"; import { Output } from "../../shared/output/output.service.ts"; import { notebooksMachineOutputRequested } from "./notebooks.output.ts"; import { sanitizeInlineName, mapHttpError } from "../../command-internal/http-errors.ts"; +import { fsErrorCause } from "../../command-internal/config-validate.ts"; import { canonicalPathForContainment, isPathContainedInRoot, @@ -130,18 +131,20 @@ const notebookFilePath = Effect.fnUntraced(function* (workdir: string, name: str * (`resolveEmailTemplateContentPath`). */ const ensureNotebookPathContained = Effect.fnUntraced(function* (workdir: string, path: string) { - const contained = yield* Effect.try({ - try: () => - isPathContainedInRoot( - canonicalPathForContainment(workdir), - canonicalPathForContainment(path), - ), - catch: (cause) => - new NotebookFileError({ + const pathService = yield* Path.Path; + const contained = yield* Effect.all([ + canonicalPathForContainment(workdir), + canonicalPathForContainment(path), + ]).pipe( + Effect.map(([root, candidate]) => isPathContainedInRoot(pathService, root, candidate)), + Effect.mapError((error) => { + const cause = fsErrorCause(error); + return new NotebookFileError({ detail: `Cannot resolve ${path}: ${cause instanceof Error ? cause.message : String(cause)}`, suggestion: "Check the notebooks path is a readable directory.", - }), - }); + }); + }), + ); if (!contained) { // Echoes the declared path rather than what it resolves to, so a hostile checkout cannot // use the error to probe where its symlink lands. diff --git a/apps/cli/src/commands/services/services-local-stack.ts b/apps/cli/src/commands/services/services-local-stack.ts index a2dfc007fd..ce8cae7491 100644 --- a/apps/cli/src/commands/services/services-local-stack.ts +++ b/apps/cli/src/commands/services/services-local-stack.ts @@ -28,43 +28,38 @@ export const stackServiceVersions = Effect.fn("services.stackServiceVersions")(f let selection: ArtifactSelection = defaultArtifactSelection(); if (Result.isFailure(context)) configError = context.failure; else { - const resolved = yield* Effect.try({ - try: () => { - const value = envOverrideMajorVersion( - context.success.config.db.major_version, + const resolved = yield* Effect.gen(function* () { + const value = yield* envOverrideMajorVersion( + context.success.config.db.major_version, + context.success.projectEnvValues, + ).pipe(Effect.mapError((cause) => cause.message)); + if (value !== 15 && value !== 17) + return yield* Effect.fail(`unsupported PostgreSQL major version: ${value}`); + const db = { + major_version: value, + orioledb_version: yield* envOverrideOrioleDBVersion( + context.success.config.db.orioledb_version, context.success.projectEnvValues, - ); - if (value !== 15 && value !== 17) - throw new Error(`unsupported PostgreSQL major version: ${value}`); - const db = { - major_version: value, - orioledb_version: envOverrideOrioleDBVersion( - context.success.config.db.orioledb_version, - context.success.projectEnvValues, + ), + }; + // A listing never resolves: it shows the locked pins, else the bundled ones. + return yield* readSelectionOrBundled({ + projectRoot: workdir, + majorVersion: db.major_version, + orioledbVersion: db.orioledb_version, + services: context.success.config.local?.services, + }).pipe( + Effect.catchTag("LockResolutionError", (error) => + Effect.sync(() => { + configError = error.message; + }).pipe( + Effect.andThen(stackArtifactSelection(db)), + Effect.mapError((cause) => new StackConfigError({ message: cause.message })), ), - }; - // A listing never resolves: it shows the locked pins, else the bundled ones. - return readSelectionOrBundled({ - projectRoot: workdir, - majorVersion: db.major_version, - orioledbVersion: db.orioledb_version, - services: context.success.config.local?.services, - }).pipe( - Effect.catchTag("LockResolutionError", (error) => - Effect.sync(() => { - configError = error.message; - }).pipe( - Effect.andThen(stackArtifactSelection(db)), - Effect.mapError((cause) => new StackConfigError({ message: cause.message })), - ), - ), - ); - }, - catch: (cause) => (cause instanceof Error ? cause.message : String(cause)), - }).pipe( - Effect.flatMap((selected) => Effect.mapError(selected, (error) => error.message)), - Effect.result, - ); + ), + Effect.mapError((error) => error.message), + ); + }).pipe(Effect.result); if (Result.isFailure(resolved)) configError = resolved.failure; else selection = resolved.success; } diff --git a/apps/cli/src/commands/start/SIDE_EFFECTS.md b/apps/cli/src/commands/start/SIDE_EFFECTS.md index 0fa868828f..643804de2b 100644 --- a/apps/cli/src/commands/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/start/SIDE_EFFECTS.md @@ -103,7 +103,7 @@ command. | project-root / `SUPABASE_ENV`-selected dotenv file | dotenv | always, same precedence chain as `stop`/`status` | | `auth.signing_keys_path` file | JSON | when configured | | `api.tls.cert_path` / `api.tls.key_path` | PEM | when `api.tls.enabled` | -| `auth.email.template.*` / `auth.email.notification.*` content files | text | when configured — for every configured template plus every ENABLED notification, resolved paths are CONFINED to the project root (symlinks dereferenced with `realpathSync`) and read-verified (bytes discarded) in an eager pre-Docker pass, REGARDLESS of `auth.enabled` (Kong mounts these unconditionally — see Notes) | +| `auth.email.template.*` / `auth.email.notification.*` content files | text | when configured — for every configured template plus every ENABLED notification, resolved paths are CONFINED to the project root (symlinks dereferenced using `fs.realPath`) and read-verified (bytes discarded) in an eager pre-Docker pass, REGARDLESS of `auth.enabled` (Kong mounts these unconditionally — see Notes) | | GCP JWT credentials file | JSON | when `analytics.backend = "bigquery"` | | `/supabase/roles.sql` | SQL | on a fresh volume (custom-roles seed) — the "Seeding globals..." message always prints first; the file itself is only read if it exists, tolerating a missing file | | `/supabase/migrations/*.sql`, `supabase/seed.sql` | SQL | on a fresh volume, via the standard migration-apply + seed pipeline | @@ -379,7 +379,7 @@ prose, not structured data. container, whose own env-file/multiline-env-script staging is unaffected by that change. - Existing local values declared under a Function import map's `scopes` are mounted read-only into Edge Runtime, and into the Studio container that shares the same resolved Function bind mounts, even when they resolve outside the nearest Git root. The Edge Runtime container additionally prunes its aggregated bind list before `docker create` — a bind already supplied verbatim by a same-mode parent bind is dropped so the bootstrap `docker cp` never sees a file bind nested inside a read-only parent (supabase/supabase#50088) — while Studio keeps the full unpruned set (its bring-up performs no `docker cp`). The mounted target itself is bound as declared; imports reached from inside an out-of-root target are not additionally bound. Edge Runtime bring-up prints a `WARN` naming each distinct out-of-root host path once; Studio's bind resolution stays silent, so with Edge Runtime excluded (`-x edge-runtime`) the mounts still reach Studio and no warning is printed. Missing targets retain Edge Runtime startup's existing skip behavior. This is NOT the same permissiveness as email template `content_path` below — a `scopes` value is an explicit opt-in mechanism, mounted `:ro`, with containment enforced on the upload side elsewhere (`functions deploy`), whereas `content_path` has no opt-in at all and is enforced at resolution time, mounted `:rw,z`. -- **Auth email `content_path` project-root containment AND readability apply here regardless of `auth.enabled`.** Kong's mount set (`resolveKongEmailTemplateMounts`, `start.handler.ts`) covers every configured `[auth.email.template.*]` entry, and every `enabled = true` `[auth.email.notification.*]` entry, unconditionally — Kong is the stack's mandatory gateway, independent of whether GoTrue itself is started. `start.handler.ts` resolves, confines (`resolveEmailTemplateContentPath`; symlinks dereferenced with `realpathSync` before the check), AND read-verifies (a discarded `FileSystem.readFileString`) that same set in one eager pass before any Docker work, in addition to the `auth.enabled`-gated read `resolveLocalConfigValues`'s own validation performs; the resulting resolved path is threaded straight into Kong's bind mount (`buildKongEmailTemplateBind` in `kong.service.ts`, which no longer re-resolves anything) rather than re-derived later, right before Kong's own `docker create`. Each template is bound `rw,z`: when the container runtime applies SELinux labels (enforcing or permissive mode) it relabels the mounted host file in place with the shared container context so Kong can read it under enforcement — a persistent change to the file's security label that survives `supabase stop` — and the option is a no-op where the runtime has no SELinux support. A path that resolves outside the project root aborts the run with `Invalid config for auth.email.
..content_path: resolves outside the project root ()`; a resolved, in-root path that cannot be read as a regular file aborts with `Invalid config for auth.email.
..content_path: ` — both before a single container is created, and both regardless of `auth.enabled` (closing the gap where a missing `content_path` would otherwise reach an unconditional Kong bind mount and the root-privileged Docker daemon would silently create a directory there). +- **Auth email `content_path` project-root containment AND readability apply here regardless of `auth.enabled`.** Kong's mount set (`resolveKongEmailTemplateMounts`, `start.handler.ts`) covers every configured `[auth.email.template.*]` entry, and every `enabled = true` `[auth.email.notification.*]` entry, unconditionally — Kong is the stack's mandatory gateway, independent of whether GoTrue itself is started. `start.handler.ts` resolves, confines (`resolveEmailTemplateContentPath`; symlinks dereferenced using `fs.realPath` before the check), AND read-verifies (a discarded `FileSystem.readFileString`) that same set in one eager pass before any Docker work, in addition to the `auth.enabled`-gated read `resolveLocalConfigValues`'s own validation performs; the resulting resolved path is threaded straight into Kong's bind mount (`buildKongEmailTemplateBind` in `kong.service.ts`, which no longer re-resolves anything) rather than re-derived later, right before Kong's own `docker create`. Each template is bound `rw,z`: when the container runtime applies SELinux labels (enforcing or permissive mode) it relabels the mounted host file in place with the shared container context so Kong can read it under enforcement — a persistent change to the file's security label that survives `supabase stop` — and the option is a no-op where the runtime has no SELinux support. A path that resolves outside the project root aborts the run with `Invalid config for auth.email.
..content_path: resolves outside the project root ()`; a resolved, in-root path that cannot be read as a regular file aborts with `Invalid config for auth.email.
..content_path: ` — both before a single container is created, and both regardless of `auth.enabled` (closing the gap where a missing `content_path` would otherwise reach an unconditional Kong bind mount and the root-privileged Docker daemon would silently create a directory there). - Docker status `created` is not considered a recoverable stopped stack: the container and named volume are preserved because the volume may not have completed its first database initialization, and `start` reports the existing not-running status instead. diff --git a/apps/cli/src/commands/start/start.gates.ts b/apps/cli/src/commands/start/start.gates.ts index 30ddbd6fea..dd3438d9f0 100644 --- a/apps/cli/src/commands/start/start.gates.ts +++ b/apps/cli/src/commands/start/start.gates.ts @@ -1,5 +1,5 @@ import type { CliConfig } from "@supabase/config"; -import { Predicate } from "effect"; +import { Effect, Predicate } from "effect"; import { dockerfileServiceImage } from "../../shared/services/dockerfile-images.ts"; import type { @@ -53,41 +53,41 @@ function asRecord(value: unknown): Record | undefined { * Evaluates every excludable start gate in one pass. Postgres and Edge Runtime are handled * separately by the caller (see this module's header). */ -export function resolveStartGates(inputs: StartGateInputs): StartGates { +export const resolveStartGates = Effect.fnUntraced(function* (inputs: StartGateInputs) { const { config, projectEnvValues, excludedKeys, document } = inputs; const isExcluded = (key: string) => excludedKeys.has(key); - const analyticsEnabled = envOverrideBool( + const analyticsEnabled = yield* envOverrideBool( "SUPABASE_ANALYTICS_ENABLED", config.analytics.enabled, "analytics.enabled", projectEnvValues, ); - const apiEnabled = envOverrideBool( + const apiEnabled = yield* envOverrideBool( "SUPABASE_API_ENABLED", config.api.enabled, "api.enabled", projectEnvValues, ); - const authEnabled = envOverrideBool( + const authEnabled = yield* envOverrideBool( "SUPABASE_AUTH_ENABLED", config.auth.enabled, "auth.enabled", projectEnvValues, ); - const inbucketEnabled = envOverrideBool( + const inbucketEnabled = yield* envOverrideBool( "SUPABASE_LOCAL_SMTP_ENABLED", config.local_smtp.enabled, "local_smtp.enabled", projectEnvValues, ); - const realtimeEnabled = envOverrideBool( + const realtimeEnabled = yield* envOverrideBool( "SUPABASE_REALTIME_ENABLED", config.realtime.enabled, "realtime.enabled", projectEnvValues, ); - const storageEnabled = envOverrideBool( + const storageEnabled = yield* envOverrideBool( "SUPABASE_STORAGE_ENABLED", config.storage.enabled, "storage.enabled", @@ -101,26 +101,26 @@ export function resolveStartGates(inputs: StartGateInputs): StartGates { const configuredImageTransformationEnabled = config.storage.image_transformation?.enabled ?? false; const imageTransformationEnabled = imageTransformationSectionPresent - ? envOverrideBool( + ? yield* envOverrideBool( "SUPABASE_STORAGE_IMAGE_TRANSFORMATION_ENABLED", configuredImageTransformationEnabled, "storage.image_transformation.enabled", projectEnvValues, ) : configuredImageTransformationEnabled; - const studioEnabled = envOverrideBool( + const studioEnabled = yield* envOverrideBool( "SUPABASE_STUDIO_ENABLED", config.studio.enabled, "studio.enabled", projectEnvValues, ); - const poolerEnabled = envOverrideBool( + const poolerEnabled = yield* envOverrideBool( "SUPABASE_DB_POOLER_ENABLED", config.db.pooler.enabled, "db.pooler.enabled", projectEnvValues, ); - const edgeRuntimeEnabled = envOverrideBool( + const edgeRuntimeEnabled = yield* envOverrideBool( "SUPABASE_EDGE_RUNTIME_ENABLED", config.edge_runtime.enabled, "edge_runtime.enabled", @@ -143,8 +143,8 @@ export function resolveStartGates(inputs: StartGateInputs): StartGates { studio: studioEnabled && !isExcluded("studio"), supavisor: poolerEnabled && !isExcluded("supavisor"), edgeRuntime: edgeRuntimeEnabled && !isExcluded("edge-runtime"), - }; -} + } satisfies StartGates; +}); const GATE_KEY_BY_SERVICE: Readonly> = { logflare: "logflare", diff --git a/apps/cli/src/commands/start/start.handler.ts b/apps/cli/src/commands/start/start.handler.ts index 5d3c6133f0..c800f3bf1d 100644 --- a/apps/cli/src/commands/start/start.handler.ts +++ b/apps/cli/src/commands/start/start.handler.ts @@ -33,7 +33,9 @@ import { aqua, yellow } from "../../command-internal/colors.ts"; import { apiTlsCertReadErrorMessage, apiTlsKeyReadErrorMessage, + ConfigValidateError, emailContentPathReadErrorMessage, + fsErrorCause, resolveApiTlsPath, resolveEmailTemplateContentPath, } from "../../command-internal/config-validate.ts"; @@ -191,33 +193,27 @@ function asRecord(value: unknown): Record | undefined { return Predicate.isObject(value) ? value : undefined; } -function tryStartConfig(thunk: () => T): Effect.Effect { - return Effect.try({ - try: thunk, - catch: (cause) => - new StartInvalidConfigError({ - message: cause instanceof Error ? cause.message : String(cause), - }), +const startConfigError = (cause: unknown) => + new StartInvalidConfigError({ + message: cause instanceof Error ? cause.message : String(cause), + }); + +const configOverrideError = (dottedFieldPath: string) => (cause: unknown) => + new StartInvalidConfigError({ + message: `invalid config for ${dottedFieldPath}: ${cause instanceof Error ? cause.message : String(cause)}`, }); -} /** - * Wraps a synchronous `envOverride*` config read that throws on a malformed value into a typed + * Maps an `envOverride*` config read's malformed-value failure into a typed * `StartInvalidConfigError`, so a bad override fails the command through the normal error path * instead of surfacing as an untyped Effect defect. */ -function wrapConfigOverride( - dottedFieldPath: string, - thunk: () => T, -): Effect.Effect { - return Effect.try({ - try: thunk, - catch: (cause) => - new StartInvalidConfigError({ - message: `invalid config for ${dottedFieldPath}: ${cause instanceof Error ? cause.message : String(cause)}`, - }), - }); -} +const wrapConfigOverride = (dottedFieldPath: string, effect: Effect.Effect) => + effect.pipe(Effect.mapError(configOverrideError(dottedFieldPath))); + +/** {@link wrapConfigOverride} for a synchronous parser that throws on a malformed value. */ +const wrapConfigParse = (dottedFieldPath: string, thunk: () => T) => + Effect.try({ try: thunk, catch: configOverrideError(dottedFieldPath) }); /** * Every value {@link buildGotrueContainerSpec} needs from `config`/`values`, minus @@ -230,20 +226,20 @@ function wrapConfigOverride( * or auth is disabled. */ -function resolveGotrueEnvInput(params: { +const resolveGotrueEnvInput = Effect.fnUntraced(function* (params: { readonly context: LocalProjectContext; readonly values: LocalConfigValues; readonly workdir: string; readonly kongContainerName: string; readonly mailpitContainerName: string; readonly resolvedEmail: ResolvedAuthEmail; -}): Omit { +}) { const { context, values, workdir, kongContainerName, mailpitContainerName, resolvedEmail } = params; const { config, projectEnvValues, loaded } = context; const document = loaded?.document; - const inbucketEnabled = envOverrideBool( + const inbucketEnabled = yield* envOverrideBool( "SUPABASE_LOCAL_SMTP_ENABLED", config.local_smtp.enabled, "local_smtp.enabled", @@ -253,7 +249,7 @@ function resolveGotrueEnvInput(params: { // when the key is merely absent from the TOML table, silently falling back to Mailpit even when // a real SMTP server is configured. `resolveAuthEmailSmtp` resolves this correctly off the raw // document. - const resolvedSmtp = resolveAuthEmailSmtp(asRecord(document?.["auth"]), projectEnvValues); + const resolvedSmtp = yield* resolveAuthEmailSmtp(asRecord(document?.["auth"]), projectEnvValues); const smtp = resolvedSmtp?.enabled === true ? { @@ -267,12 +263,12 @@ function resolveGotrueEnvInput(params: { : undefined; // Same override gap as `inbucketEnabled` above; these are value-typed fields, so no // raw-document presence gate is needed. - const mailpitAdminEmail = envOverride( + const mailpitAdminEmail = yield* envOverride( "SUPABASE_LOCAL_SMTP_ADMIN_EMAIL", config.local_smtp.admin_email, projectEnvValues, ); - const mailpitSenderName = envOverride( + const mailpitSenderName = yield* envOverride( "SUPABASE_LOCAL_SMTP_SENDER_NAME", config.local_smtp.sender_name, projectEnvValues, @@ -286,13 +282,16 @@ function resolveGotrueEnvInput(params: { } : undefined; - const { passkeyEnabled, webauthn } = resolveGotruePasskeyWebauthn(document, projectEnvValues); - const externalProviders = resolveAuthExternalProviders( + const { passkeyEnabled, webauthn } = yield* resolveGotruePasskeyWebauthn( + document, + projectEnvValues, + ); + const externalProviders = yield* resolveAuthExternalProviders( asRecord(document?.["auth"]), config.auth.external, projectEnvValues, ); - const authExternalUrl = resolveAuthExternalUrl(document, projectEnvValues); + const authExternalUrl = yield* resolveAuthExternalUrl(document, projectEnvValues); return { apiUrl: values.apiUrl, @@ -313,14 +312,18 @@ function resolveGotrueEnvInput(params: { kongContainerName, smtp, mailpit, - sms: resolveAuthSms(asRecord(document?.["auth"]), config.auth.sms, projectEnvValues), - sessions: resolveGotrueSessions(config.auth.sessions, projectEnvValues), - mfa: resolveAuthMfa(config.auth.mfa, projectEnvValues), - rateLimit: resolveGotrueRateLimit(config.auth.rate_limit, projectEnvValues), - web3: resolveGotrueWeb3(config.auth.web3, projectEnvValues), - oauthServer: resolveGotrueOAuthServer(config.auth.oauth_server, projectEnvValues), - hooks: resolveAuthHooks(asRecord(document?.["auth"]), config.auth.hook, projectEnvValues), - captcha: resolveAuthCaptcha( + sms: yield* resolveAuthSms(asRecord(document?.["auth"]), config.auth.sms, projectEnvValues), + sessions: yield* resolveGotrueSessions(config.auth.sessions, projectEnvValues), + mfa: yield* resolveAuthMfa(config.auth.mfa, projectEnvValues), + rateLimit: yield* resolveGotrueRateLimit(config.auth.rate_limit, projectEnvValues), + web3: yield* resolveGotrueWeb3(config.auth.web3, projectEnvValues), + oauthServer: yield* resolveGotrueOAuthServer(config.auth.oauth_server, projectEnvValues), + hooks: yield* resolveAuthHooks( + asRecord(document?.["auth"]), + config.auth.hook, + projectEnvValues, + ), + captcha: yield* resolveAuthCaptcha( asRecord(document?.["auth"]), config.auth.captcha, projectEnvValues, @@ -328,9 +331,9 @@ function resolveGotrueEnvInput(params: { passkeyEnabled, webauthn, externalProviders, - signingKeys: resolveConfiguredSigningKeys(config, workdir, projectEnvValues), - }; -} + signingKeys: yield* resolveConfiguredSigningKeys(config, workdir, projectEnvValues), + } satisfies Omit; +}); /** * Existence/readability check for one already-resolved `content_path`. Without it, a @@ -378,14 +381,16 @@ const resolveKongEmailTemplateMounts = Effect.fn("start.resolveEmailTemplateMoun name: string, contentPath: string, ) => - tryStartConfig(() => - resolveEmailTemplateContentPath({ - section, - name, - contentPath, - contentPresent: false, - base: workdir, - }), + resolveEmailTemplateContentPath({ + section, + name, + contentPath, + contentPresent: false, + base: workdir, + }).pipe( + Effect.mapError((error) => + startConfigError(error instanceof ConfigValidateError ? error : fsErrorCause(error)), + ), ); const mounts: Array = []; for (const [id, template] of Object.entries(email.template)) { @@ -454,28 +459,24 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { cliSettings.workdir, (message) => new StartConfigLoadError({ message }), ); - const values = yield* tryStartConfig(() => - resolveLocalConfigValues( - context.config, - context.hostname, - cliSettings.workdir, - context.projectEnvValues, - context.loaded?.document, - ), - ); + const values = yield* resolveLocalConfigValues( + context.config, + context.hostname, + cliSettings.workdir, + context.projectEnvValues, + context.loaded?.document, + ).pipe(Effect.mapError(startConfigError)); const { config, projectId, projectEnvValues } = context; // Resolved here (project `.env` aware) so it can be threaded through to `startDatabase`'s // own `setup.experimental` below. const experimental = yield* resolveExperimentalWithProjectEnv(projectEnvValues); // Single source resolved once, fed to both Kong's template mounts and GoTrue's env builder — // see {@link resolveAuthEmail}'s doc comment. - const resolvedEmail = yield* tryStartConfig(() => - resolveAuthEmail( - config.auth.email, - asRecord(context.loaded?.document?.["auth"]), - projectEnvValues, - ), - ); + const resolvedEmail = yield* resolveAuthEmail( + config.auth.email, + asRecord(context.loaded?.document?.["auth"]), + projectEnvValues, + ).pipe(Effect.mapError(startConfigError)); // Kong mounts every configured template and every enabled notification's `content_path`, // regardless of `auth.enabled` — see {@link resolveKongEmailTemplateMounts} for why this // resolves and verifies each path once, here, rather than re-deriving it before Kong's @@ -488,23 +489,21 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // Duration fields (Go duration syntax) are otherwise only parsed inside GoTrue's own env // builder, which never runs when auth is disabled or `gotrue` is excluded — so a malformed // value must be validated eagerly here or it would be silently accepted. - const gotrueSessionsForValidation = resolveGotrueSessions( + const gotrueSessionsForValidation = yield* resolveGotrueSessions( config.auth.sessions, projectEnvValues, ); - yield* wrapConfigOverride("auth.email.max_frequency", () => + yield* wrapConfigParse("auth.email.max_frequency", () => parseGoDuration(resolvedEmail.max_frequency), ); // `resolveLocalConfigValues`'s own SMS validation only runs when auth is enabled, so this is // the only place a malformed `auth.sms.*` override is caught when auth is disabled. - const smsForValidation = yield* tryStartConfig(() => - resolveAuthSms( - asRecord(context.loaded?.document?.["auth"]), - config.auth.sms, - projectEnvValues, - ), - ); - yield* wrapConfigOverride("auth.sms.max_frequency", () => + const smsForValidation = yield* resolveAuthSms( + asRecord(context.loaded?.document?.["auth"]), + config.auth.sms, + projectEnvValues, + ).pipe(Effect.mapError(startConfigError)); + yield* wrapConfigParse("auth.sms.max_frequency", () => parseGoDuration(smsForValidation.max_frequency), ); // `resolveAuthSms` already downgrades `enable_signup` when no provider is enabled; this only @@ -515,47 +514,53 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { !smsForValidation.messagebird.enabled && !smsForValidation.textlocal.enabled && !smsForValidation.vonage.enabled && - envOverrideBool( + (yield* envOverrideBool( "SUPABASE_AUTH_SMS_ENABLE_SIGNUP", config.auth.sms.enable_signup, "auth.sms.enable_signup", projectEnvValues, - ) + ).pipe(Effect.orDie)) ) { yield* output.raw("WARN: no SMS provider is enabled. Disabling phone login\n", "stderr"); } const sessionsTimebox = gotrueSessionsForValidation?.timebox; if (sessionsTimebox !== undefined) { - yield* wrapConfigOverride("auth.sessions.timebox", () => parseGoDuration(sessionsTimebox)); + yield* wrapConfigParse("auth.sessions.timebox", () => parseGoDuration(sessionsTimebox)); } const sessionsInactivityTimeout = gotrueSessionsForValidation?.inactivity_timeout; if (sessionsInactivityTimeout !== undefined) { - yield* wrapConfigOverride("auth.sessions.inactivity_timeout", () => + yield* wrapConfigParse("auth.sessions.inactivity_timeout", () => parseGoDuration(sessionsInactivityTimeout), ); } - yield* wrapConfigOverride("auth.mfa.phone.max_frequency", () => - parseGoDuration(resolveAuthMfa(config.auth.mfa, projectEnvValues).phone.max_frequency), + const mfaForValidation = yield* wrapConfigOverride( + "auth.mfa.phone.max_frequency", + resolveAuthMfa(config.auth.mfa, projectEnvValues), + ); + yield* wrapConfigParse("auth.mfa.phone.max_frequency", () => + parseGoDuration(mfaForValidation.phone.max_frequency), ); // These GoTrue overrides must validate unconditionally too, regardless of - // `auth.enabled`/`--exclude gotrue`. The resolvers already throw internally on a bad + // `auth.enabled`/`--exclude gotrue`. The resolvers already fail internally on a bad // override, so calling each once here is simpler than re-deriving every field individually. - yield* wrapConfigOverride("auth.rate_limit", () => + yield* wrapConfigOverride( + "auth.rate_limit", resolveGotrueRateLimit(config.auth.rate_limit, projectEnvValues), ); - yield* wrapConfigOverride("auth.web3", () => - resolveGotrueWeb3(config.auth.web3, projectEnvValues), - ); - yield* wrapConfigOverride("auth.oauth_server", () => + yield* wrapConfigOverride("auth.web3", resolveGotrueWeb3(config.auth.web3, projectEnvValues)); + yield* wrapConfigOverride( + "auth.oauth_server", resolveGotrueOAuthServer(config.auth.oauth_server, projectEnvValues), ); // Same gap for the raw (unmodeled by `@supabase/config`) `auth.passkey`/`auth.webauthn`/ // `auth.external.` booleans, which are otherwise only reached once auth is enabled and // gotrue isn't excluded. - yield* wrapConfigOverride("auth.passkey", () => + yield* wrapConfigOverride( + "auth.passkey", resolveGotruePasskeyWebauthn(context.loaded?.document, projectEnvValues), ); - yield* wrapConfigOverride("auth.external", () => + yield* wrapConfigOverride( + "auth.external", resolveAuthExternalProviders( asRecord(context.loaded?.document?.["auth"]), config.auth.external, @@ -564,7 +569,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { ); // Same gap for `auth.third_party..*` — `resolveThirdPartyProviders` is otherwise // never called by this handler at all, so a malformed override would never fail the command. - yield* wrapConfigOverride("auth.third_party", () => + yield* wrapConfigOverride( + "auth.third_party", resolveThirdPartyProviders(config.auth.third_party, projectEnvValues), ); // `[functions..env]` has no supported meaning for `start`, so any key here must be @@ -599,21 +605,21 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { excluded: ReadonlyArray, precomputedLocal?: LocalConfigValues, ) { - const localState = yield* Effect.try({ - try: () => - resolveStatusLocalState( - context.config, - context.hostname, - cliSettings.workdir, - context.projectEnvValues, - context.loaded?.document, - precomputedLocal, - ), - catch: (cause) => - new StatusInvalidConfigError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); + const localState = yield* resolveStatusLocalState( + context.config, + context.hostname, + cliSettings.workdir, + context.projectEnvValues, + context.loaded?.document, + precomputedLocal, + ).pipe( + Effect.mapError( + (cause) => + new StatusInvalidConfigError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + ), + ); const containerIds = statusContainerIds(projectId); const state = gateStatusState(localState, containerIds, excluded); return statusValuesFromState(state, new Map()); @@ -707,16 +713,14 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // 4. No update-suggestion check: `start` has no Management API dependency by design. - // 5. Gate evaluation — see `start.gates.ts`. Wrapped because `envOverrideBool` throws - // synchronously on an unparsable value, and this handler surfaces that as a typed error. - const gates = yield* tryStartConfig(() => - resolveStartGates({ - config, - projectEnvValues, - excludedKeys, - document: context.loaded?.document, - }), - ); + // 5. Gate evaluation — see `start.gates.ts`. Mapped because `envOverrideBool` fails on + // an unparsable value, and this handler surfaces that as a `StartInvalidConfigError`. + const gates = yield* resolveStartGates({ + config, + projectEnvValues, + excludedKeys, + document: context.loaded?.document, + }).pipe(Effect.mapError(startConfigError)); // 6. JWKS resolution runs unconditionally, before any image pull, regardless of which // services end up enabled. @@ -729,7 +733,10 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // The `edge_runtime.deno_version` -> image switch is start-only (no `db start` equivalent), // so it's resolved here rather than inside the shared bootstrap-config derivation below. - const denoVersion = envOverrideDenoVersion(config.edge_runtime.deno_version, projectEnvValues); + const denoVersion = yield* envOverrideDenoVersion( + config.edge_runtime.deno_version, + projectEnvValues, + ).pipe(Effect.orDie); // Every field the fresh-DB bootstrap needs, shared with `db start`'s own native bootstrap — // see `bootstrap-config.ts`'s header for why this is one shared derivation. @@ -875,13 +882,15 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // The TLS cert/key disk read is gated on `api.enabled` itself, not just `api.tls.enabled`. // Resolved separately from `gates.postgrest`'s own `apiEnabled`, which is additionally // combined with `--exclude postgrest` — a distinction config validation has no equivalent for. - const apiEnabled = yield* wrapConfigOverride("api.enabled", () => + const apiEnabled = yield* wrapConfigOverride( + "api.enabled", envOverrideBool("SUPABASE_API_ENABLED", config.api.enabled, "api.enabled", projectEnvValues), ); // The post-bring-up health-probe CA-trust lookup needs this same env-overridden value, not // the raw `config.api.tls.enabled` — both the trust pool and its target URL must read one // source of truth. - const apiTlsEnabled = yield* wrapConfigOverride("api.tls.enabled", () => + const apiTlsEnabled = yield* wrapConfigOverride( + "api.tls.enabled", envOverrideBool( "SUPABASE_API_TLS_ENABLED", config.api.tls.enabled, @@ -891,12 +900,12 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { ); // Same override gap as `apiTlsEnabled` above: the env overrides must apply before reading // the cert/key paths from disk. - const apiTlsCertPath = envOverride( + const apiTlsCertPath = yield* envOverride( "SUPABASE_API_TLS_CERT_PATH", config.api.tls.cert_path, projectEnvValues, ); - const apiTlsKeyPath = envOverride( + const apiTlsKeyPath = yield* envOverride( "SUPABASE_API_TLS_KEY_PATH", config.api.tls.key_path, projectEnvValues, @@ -914,7 +923,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { apiTlsKeyPath.length > 0 ) { tlsCertContent = yield* fs - .readFileString(resolveApiTlsPath(cliSettings.workdir, apiTlsCertPath)) + .readFileString(resolveApiTlsPath(path, cliSettings.workdir, apiTlsCertPath)) .pipe( Effect.mapError( (cause) => @@ -924,7 +933,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { ), ); tlsKeyContent = yield* fs - .readFileString(resolveApiTlsPath(cliSettings.workdir, apiTlsKeyPath)) + .readFileString(resolveApiTlsPath(path, cliSettings.workdir, apiTlsKeyPath)) .pipe( Effect.mapError( (cause) => @@ -937,7 +946,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // Same gap for `storage.vector.enabled` — both the Storage container and `seedBucketsRun`'s // config splice further down must see the same already-overridden value. - const storageVectorEnabled = yield* wrapConfigOverride("storage.vector.enabled", () => + const storageVectorEnabled = yield* wrapConfigOverride( + "storage.vector.enabled", envOverrideBool( "SUPABASE_STORAGE_VECTOR_ENABLED", config.storage.vector.enabled, @@ -947,7 +957,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { ); // Same gap for `storage.s3_protocol.enabled`: the Storage spec builder only parsed this // lazily, so a malformed override was silently accepted whenever Storage is excluded/disabled. - const storageS3ProtocolEnabled = yield* wrapConfigOverride("storage.s3_protocol.enabled", () => + const storageS3ProtocolEnabled = yield* wrapConfigOverride( + "storage.s3_protocol.enabled", envOverrideBool( "SUPABASE_STORAGE_S3_PROTOCOL_ENABLED", config.storage.s3_protocol.enabled, @@ -958,7 +969,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // Same gap for `storage.analytics.enabled`. `start` never reads this field itself (only // `seed buckets --linked` does), so it's validated here purely for fail-fast parity and the // result discarded. - yield* wrapConfigOverride("storage.analytics.enabled", () => + yield* wrapConfigOverride( + "storage.analytics.enabled", envOverrideBool( "SUPABASE_STORAGE_ANALYTICS_ENABLED", config.storage.analytics.enabled, @@ -968,7 +980,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { ); // These plain `uint` fields must validate unconditionally too. `start` never reads them // itself (only `config push`/`pull` do), so they're validated purely for fail-fast parity. - yield* wrapConfigOverride("storage.analytics.max_namespaces", () => + yield* wrapConfigOverride( + "storage.analytics.max_namespaces", envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_NAMESPACES", "storage.analytics.max_namespaces", @@ -976,7 +989,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { projectEnvValues, ), ); - yield* wrapConfigOverride("storage.analytics.max_tables", () => + yield* wrapConfigOverride( + "storage.analytics.max_tables", envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_TABLES", "storage.analytics.max_tables", @@ -984,7 +998,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { projectEnvValues, ), ); - yield* wrapConfigOverride("storage.analytics.max_catalogs", () => + yield* wrapConfigOverride( + "storage.analytics.max_catalogs", envOverrideUint( "SUPABASE_STORAGE_ANALYTICS_MAX_CATALOGS", "storage.analytics.max_catalogs", @@ -992,7 +1007,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { projectEnvValues, ), ); - yield* wrapConfigOverride("storage.vector.max_buckets", () => + yield* wrapConfigOverride( + "storage.vector.max_buckets", envOverrideUint( "SUPABASE_STORAGE_VECTOR_MAX_BUCKETS", "storage.vector.max_buckets", @@ -1000,7 +1016,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { projectEnvValues, ), ); - yield* wrapConfigOverride("storage.vector.max_indexes", () => + yield* wrapConfigOverride( + "storage.vector.max_indexes", envOverrideUint( "SUPABASE_STORAGE_VECTOR_MAX_INDEXES", "storage.vector.max_indexes", @@ -1011,10 +1028,14 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // Same gap for `api.schemas`/`api.extra_search_path`/`api.max_rows` — both PostgREST's own // container and Studio's copy of the same `PGRST_DB_*` env must see the overridden values. - const apiSchemasOverride = envOverride("SUPABASE_API_SCHEMAS", undefined, projectEnvValues); + const apiSchemasOverride = yield* envOverride( + "SUPABASE_API_SCHEMAS", + undefined, + projectEnvValues, + ); const apiSchemas = apiSchemasOverride !== undefined ? apiSchemasOverride.split(",") : config.api.schemas; - const apiExtraSearchPathOverride = envOverride( + const apiExtraSearchPathOverride = yield* envOverride( "SUPABASE_API_EXTRA_SEARCH_PATH", undefined, projectEnvValues, @@ -1023,13 +1044,15 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { apiExtraSearchPathOverride !== undefined ? apiExtraSearchPathOverride.split(",") : config.api.extra_search_path; - const apiMaxRows = yield* wrapConfigOverride("api.max_rows", () => + const apiMaxRows = yield* wrapConfigOverride( + "api.max_rows", envOverrideApiMaxRows(config.api.max_rows, projectEnvValues), ); // Same gap for Mailpit's three ports. `smtp_port`/`pop3_port` have no TOML default, so `?? 0` // here preserves the "unconfigured" signal `mailpit.service.ts`'s `!== 0` publish guard checks. - const mailpitPort = yield* wrapConfigOverride("local_smtp.port", () => + const mailpitPort = yield* wrapConfigOverride( + "local_smtp.port", envOverridePort( "SUPABASE_LOCAL_SMTP_PORT", config.local_smtp.port, @@ -1037,7 +1060,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { projectEnvValues, ), ); - const mailpitSmtpPort = yield* wrapConfigOverride("local_smtp.smtp_port", () => + const mailpitSmtpPort = yield* wrapConfigOverride( + "local_smtp.smtp_port", envOverridePort( "SUPABASE_LOCAL_SMTP_SMTP_PORT", config.local_smtp.smtp_port ?? 0, @@ -1045,7 +1069,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { projectEnvValues, ), ); - const mailpitPop3Port = yield* wrapConfigOverride("local_smtp.pop3_port", () => + const mailpitPop3Port = yield* wrapConfigOverride( + "local_smtp.pop3_port", envOverridePort( "SUPABASE_LOCAL_SMTP_POP3_PORT", config.local_smtp.pop3_port ?? 0, @@ -1056,7 +1081,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // Same gap for Logflare's port — `SUPABASE_ANALYTICS_PORT` must apply // before building Logflare's host port binding. - const analyticsPort = yield* wrapConfigOverride("analytics.port", () => + const analyticsPort = yield* wrapConfigOverride( + "analytics.port", envOverridePort( "SUPABASE_ANALYTICS_PORT", config.analytics.port, @@ -1066,7 +1092,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { ); // Same gap for Logflare's deprecated `analytics.vector_port`: nothing downstream reads the // resolved value, but a malformed override must still fail before any Docker work. - yield* wrapConfigOverride("analytics.vector_port", () => + yield* wrapConfigOverride( + "analytics.vector_port", envOverridePort( "SUPABASE_ANALYTICS_VECTOR_PORT", config.analytics.vector_port ?? 0, @@ -1078,7 +1105,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // Same gap for Supavisor's pooler fields — `pool_mode` specifically decides the published // host port (5432 session vs 6543 transaction). Wrapped via `wrapConfigOverride` so a bad // value fails as a typed error instead of an untyped Effect defect. - const poolerPort = yield* wrapConfigOverride("db.pooler.port", () => + const poolerPort = yield* wrapConfigOverride( + "db.pooler.port", envOverridePort( "SUPABASE_DB_POOLER_PORT", config.db.pooler.port, @@ -1086,13 +1114,16 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { projectEnvValues, ), ); - const poolMode = yield* wrapConfigOverride("db.pooler.pool_mode", () => + const poolMode = yield* wrapConfigOverride( + "db.pooler.pool_mode", envOverridePoolMode(config.db.pooler.pool_mode, projectEnvValues), ); - const poolerDefaultPoolSize = yield* wrapConfigOverride("db.pooler.default_pool_size", () => + const poolerDefaultPoolSize = yield* wrapConfigOverride( + "db.pooler.default_pool_size", envOverrideDefaultPoolSize(config.db.pooler.default_pool_size, projectEnvValues), ); - const poolerMaxClientConn = yield* wrapConfigOverride("db.pooler.max_client_conn", () => + const poolerMaxClientConn = yield* wrapConfigOverride( + "db.pooler.max_client_conn", envOverrideMaxClientConn(config.db.pooler.max_client_conn, projectEnvValues), ); @@ -1100,10 +1131,12 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // regardless of `--exclude edge-runtime`. The Edge Runtime branch below re-resolves both // against the env-interpolated subtree for the real container build; this eager call only // needs the raw config value to prove it parses. - const edgeRuntimePolicy = yield* wrapConfigOverride("edge_runtime.policy", () => + const edgeRuntimePolicy = yield* wrapConfigOverride( + "edge_runtime.policy", envOverrideEdgeRuntimePolicy(config.edge_runtime.policy, projectEnvValues), ); - const edgeRuntimeInspectorPort = yield* wrapConfigOverride("edge_runtime.inspector_port", () => + const edgeRuntimeInspectorPort = yield* wrapConfigOverride( + "edge_runtime.inspector_port", envOverridePort( "SUPABASE_EDGE_RUNTIME_INSPECTOR_PORT", config.edge_runtime.inspector_port, @@ -1220,14 +1253,14 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { projectId, networkId, dbUrl: values.dbUrl, - env: resolveGotrueEnvInput({ + env: yield* resolveGotrueEnvInput({ context, values, workdir: cliSettings.workdir, kongContainerName, mailpitContainerName, resolvedEmail, - }), + }).pipe(Effect.orDie), }), }; @@ -1332,11 +1365,11 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { kongContainerName, logflareContainerName, studioApiUrl: resolveStudioApiUrl( - envOverride( + (yield* envOverride( "SUPABASE_STUDIO_API_URL", config.studio.api_url, projectEnvValues, - ) ?? config.studio.api_url, + )) ?? config.studio.api_url, context.hostname, values.apiUrl, ), @@ -1430,7 +1463,10 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // `postgresExtraEnv` reads this and its sibling S3 fields for its // `POSTGRES_INITDB_ARGS` branch. orioledb_version: orioledbVersion, - settings: resolveDbSettingsEnvOverrides(config.db.settings, projectEnvValues), + settings: yield* resolveDbSettingsEnvOverrides( + config.db.settings, + projectEnvValues, + ).pipe(Effect.orDie), }, experimental: { ...config.experimental, @@ -1486,7 +1522,10 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // long-running Realtime/GoTrue/PostgREST containers too), so it's reused, not re-resolved. jwks: Effect.succeed(jwks), apiUrl: values.apiUrl, - authExternalUrl: resolveAuthExternalUrl(context.loaded?.document, projectEnvValues), + authExternalUrl: yield* resolveAuthExternalUrl( + context.loaded?.document, + projectEnvValues, + ), siteUrl: values.authSiteUrl, anonKey: values.anonKey, serviceRoleKey: values.serviceRoleKey, diff --git a/apps/cli/src/commands/start/start.services.unit.test.ts b/apps/cli/src/commands/start/start.services.unit.test.ts index 98d7f7b7ac..f49310fa17 100644 --- a/apps/cli/src/commands/start/start.services.unit.test.ts +++ b/apps/cli/src/commands/start/start.services.unit.test.ts @@ -1,5 +1,5 @@ import { CliConfigSchema, type CliConfig } from "@supabase/config"; -import { Predicate, Schema } from "effect"; +import { Effect, Predicate, Schema } from "effect"; import { assert, describe, expect, it } from "vitest"; import { dockerfileServiceImageRaw } from "../../shared/services/dockerfile-images.ts"; @@ -180,12 +180,14 @@ describe("START_SERVICES enabledGate cross-check against start.gates.ts", () => /** Real gates for `config`, with the exclusion factor neutralized (nothing excluded). */ function realGatesFor(config: CliConfig): StartGates { - return resolveStartGates({ - config, - projectEnvValues: undefined, - excludedKeys: new Set(), - document: undefined, - }); + return Effect.runSync( + resolveStartGates({ + config, + projectEnvValues: undefined, + excludedKeys: new Set(), + document: undefined, + }), + ); } function expectGatesMatchMetadata(config: CliConfig, label: string) { diff --git a/apps/cli/src/commands/status/SIDE_EFFECTS.md b/apps/cli/src/commands/status/SIDE_EFFECTS.md index 9530f57632..f6267ac51e 100644 --- a/apps/cli/src/commands/status/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/status/SIDE_EFFECTS.md @@ -23,7 +23,7 @@ which branch they linked) can discover which project/branch it's on without a se | `/supabase/config.toml` | TOML | always, to resolve project configuration | | `auth.signing_keys_path` (config-relative or absolute) | JSON | only when `auth.signing_keys_path` is set in config.toml | | `api.tls.cert_path` / `api.tls.key_path` (unconditionally joined with `/supabase`, no absolute-path guard) | raw bytes | only when `api.enabled` and `api.tls.enabled`, and the respective path is set | -| `auth.email.template.*` / `auth.email.notification.*` `content_path` (config-relative or absolute) | text (existence/readability only — bytes discarded, used only to validate the config) | only when `auth.enabled`, for every configured template and every notification with `enabled = true`; the resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) — a path resolving outside it aborts before the read | +| `auth.email.template.*` / `auth.email.notification.*` `content_path` (config-relative or absolute) | text (existence/readability only — bytes discarded, used only to validate the config) | only when `auth.enabled`, for every configured template and every notification with `enabled = true`; the resolved path is CONFINED to the project root (symlinks dereferenced using `fs.realPath`) — a path resolving outside it aborts before the read | | `/supabase/.temp/project-ref` | plain text | always (soft) — the linked-state "currently linked ref" lookup (CLI-2167 follow-up, TS-only) | | `/supabase/.temp/linked-project.json` | JSON | always (soft), once linked — determines plain-project-vs-branch state and the display name (CLI-2167 follow-up, TS-only) | diff --git a/apps/cli/src/commands/status/status.handler.ts b/apps/cli/src/commands/status/status.handler.ts index 6eb5f46efe..57d79c79f9 100644 --- a/apps/cli/src/commands/status/status.handler.ts +++ b/apps/cli/src/commands/status/status.handler.ts @@ -141,20 +141,20 @@ export const status = Effect.fn("status")(function* (flags: StatusFlags) { // 3. Config validation runs entirely before the health check/container listing below, so a // config error (`InvalidJwtSecretError`, a malformed `SUPABASE_*_PORT`/`_ENABLED` override, a // signing-keys-file error) fails here rather than being masked by a Docker/DB error. - const localState = yield* Effect.try({ - try: () => - resolveStatusLocalState( - context.config, - context.hostname, - cliSettings.workdir, - context.projectEnvValues, - context.loaded?.document, - ), - catch: (cause) => - new StatusInvalidConfigError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); + const localState = yield* resolveStatusLocalState( + context.config, + context.hostname, + cliSettings.workdir, + context.projectEnvValues, + context.loaded?.document, + ).pipe( + Effect.mapError( + (cause) => + new StatusInvalidConfigError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + ), + ); // 4. `status` has no --project-id flag: resolution is env → toml → workdir basename, then // sanitized to match the same string the Docker label `start` writes (see @@ -224,7 +224,7 @@ export const status = Effect.fn("status")(function* (flags: StatusFlags) { // The pretty renderer always uses a fresh, empty override map — `--override-name` only // affects the env/json/toml/yaml path, never the pretty table — remapped from the - // already-resolved `state` so it stays consistent without a second (throwing) resolution. + // already-resolved `state` so it stays consistent without a second (fallible) resolution. const renderPretty = Effect.fnUntraced(function* () { yield* output.raw(`${aqua("supabase")} local development setup is running.\n\n`, "stderr"); const pretty = statusValuesFromState(state, new Map()); diff --git a/apps/cli/src/commands/stop/SIDE_EFFECTS.md b/apps/cli/src/commands/stop/SIDE_EFFECTS.md index bd206c8d98..f541f79fc1 100644 --- a/apps/cli/src/commands/stop/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/stop/SIDE_EFFECTS.md @@ -21,7 +21,7 @@ model (see the CLI-1324 plan's "Critical architectural finding" for why). | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/supabase/config.json` or `config.toml` | JSON or TOML | backend routing before command parsing when `SUPABASE_EXPERIMENTAL_STACK` is unset or empty; JSON takes precedence when both exist | | `/supabase/config.toml` | TOML | legacy handler default path only; skipped with `--project-id` or `--all` | -| `auth.email.template.*` / `auth.email.notification.*` `content_path` (config-relative or absolute) | text (existence/readability only — bytes discarded, used only to validate the config) | default path only, only when `auth.enabled`, for every configured template and every notification with `enabled = true`, as part of `resolveLocalConfigValues`'s own `Config.Validate` pass; the resolved path is CONFINED to the project root (symlinks dereferenced with `realpathSync`) — a path resolving outside it aborts before the read | +| `auth.email.template.*` / `auth.email.notification.*` `content_path` (config-relative or absolute) | text (existence/readability only — bytes discarded, used only to validate the config) | default path only, only when `auth.enabled`, for every configured template and every notification with `enabled = true`, as part of `resolveLocalConfigValues`'s own `Config.Validate` pass; the resolved path is CONFINED to the project root (symlinks dereferenced using `fs.realPath`) — a path resolving outside it aborts before the read | ## Files Written diff --git a/apps/cli/src/commands/stop/stop.handler.ts b/apps/cli/src/commands/stop/stop.handler.ts index 0fa66dd17c..23525ed499 100644 --- a/apps/cli/src/commands/stop/stop.handler.ts +++ b/apps/cli/src/commands/stop/stop.handler.ts @@ -60,22 +60,22 @@ const resolveSearchProjectIdFilter = Effect.fn("stop.resolveSearchProjectIdFilte // Runs full config validation before touching Docker, unlike the `--all`/`--project-id` // branches above which bypass config loading. `resolveLocalConfigValues` is reused purely for - // its throwing side effects — its resolved URLs/keys are discarded — giving `stop` the same + // its validation failures — its resolved URLs/keys are discarded — giving `stop` the same // config-validation coverage `status` already has. - yield* Effect.try({ - try: () => - resolveLocalConfigValues( - context.config, - context.hostname, - cliSettings.workdir, - context.projectEnvValues, - context.loaded?.document, - ), - catch: (cause) => - new StopConfigLoadError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); + yield* resolveLocalConfigValues( + context.config, + context.hostname, + cliSettings.workdir, + context.projectEnvValues, + context.loaded?.document, + ).pipe( + Effect.mapError( + (cause) => + new StopConfigLoadError({ + message: cause instanceof Error ? cause.message : String(cause), + }), + ), + ); return context.projectId; }); From 391a5cd2164be34cf0dd9da39cf30e415d4e5042 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:03:17 +0000 Subject: [PATCH 18/33] refactor(api): cover the client sources with effect lint (CLI-2841) (#7309) ## TL;DR brings `packages/api/src` and its tests under the effect lint ## whats introduced? - `packages/api/src/**` joins the allow list, and the generated `contracts.ts` stays out - `SupabaseApiConfigError` and `SupabaseApiInputError` move to `Data.TaggedError` with the same message, name, cause, json and printed output, pinned by a new test, so two `apps/cli` call sites whose failures include them now yield them directly - `createApiClient` in `bun.ts` and `node.ts` chains its promise instead of being `async`, still disposing the runtime before it rejects - the src tests move to effect: `FileSystem` and `Path`, schema json codecs, and `Effect.runPromise` bodies instead of `async` callbacks ## ref - closes: CLI-2841 --- .oxlintrc.effect.json | 2 + apps/cli/src/command-internal/http-errors.ts | 2 +- apps/cli/src/commands/start/start.handler.ts | 2 +- .../api/src/bun-lifecycle.integration.test.ts | 39 +- packages/api/src/bun.ts | 13 +- .../src/config/api-config.layer.unit.test.ts | 36 +- packages/api/src/effect.unit.test.ts | 841 +++--- packages/api/src/entrypoints.unit.test.ts | 65 +- .../src/generated-contract-sync.unit.test.ts | 29 +- packages/api/src/internal/client.ts | 50 +- packages/api/src/internal/client.unit.test.ts | 2678 +++++++++-------- packages/api/src/internal/effect-client.ts | 4 +- packages/api/src/internal/promise-client.ts | 10 +- .../src/internal/promise-client.unit.test.ts | 187 +- .../src/node-lifecycle.integration.test.ts | 190 +- packages/api/src/node.ts | 13 +- 16 files changed, 2150 insertions(+), 2011 deletions(-) diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index ad8cb1feb6..4f1508de43 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -6,6 +6,7 @@ }, "ignorePatterns": [ "**", + "!packages/api/src/**", "!packages/config/**", "!packages/stack/**", "!apps/cli/src/**", @@ -16,6 +17,7 @@ "apps/cli/src/shared/feedback/database.types.ts", "apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts", "apps/cli/src/command-internal/db-bootstrap/local-db-running.ts", + "packages/api/src/generated/contracts.ts", "!apps/cli/tests/helpers/branches-live.ts", "!apps/cli/tests/helpers/compute.ts", "!apps/cli/tests/helpers/migration-live.ts", diff --git a/apps/cli/src/command-internal/http-errors.ts b/apps/cli/src/command-internal/http-errors.ts index 168deb48d0..136945f5a8 100644 --- a/apps/cli/src/command-internal/http-errors.ts +++ b/apps/cli/src/command-internal/http-errors.ts @@ -157,7 +157,7 @@ export function mapHttpError(opts: { if (cause instanceof SupabaseApiInputError || cause instanceof HttpBody.HttpBodyError) { // Client-side validation/build failures: keep their identity, since // this generic mapper can't safely reclassify them as response errors. - return yield* Effect.fail(cause); + return yield* cause; } if (HttpClientError.isHttpClientError(cause)) { if (RESPONSE_ERROR_TAGS.has(cause.reason._tag) && cause.response !== undefined) { diff --git a/apps/cli/src/commands/start/start.handler.ts b/apps/cli/src/commands/start/start.handler.ts index c800f3bf1d..cf6764dd03 100644 --- a/apps/cli/src/commands/start/start.handler.ts +++ b/apps/cli/src/commands/start/start.handler.ts @@ -1851,7 +1851,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { if (Result.isFailure(seedResult)) { // No manual `rollbackStart` here — the outer // `Effect.onError` below rolls back on this failure too. - return yield* Effect.fail(seedResult.failure); + return yield* seedResult.failure; } } } diff --git a/packages/api/src/bun-lifecycle.integration.test.ts b/packages/api/src/bun-lifecycle.integration.test.ts index 3c381d27d2..91c84c1577 100644 --- a/packages/api/src/bun-lifecycle.integration.test.ts +++ b/packages/api/src/bun-lifecycle.integration.test.ts @@ -1,8 +1,9 @@ import { describe, expect, test } from "vitest"; +import { Effect } from "effect"; import { createApiClient } from "./bun.ts"; describe("Bun client lifecycle", () => { - test("serves requests through the Bun entrypoint and rejects operations after disposal", async () => { + test("serves requests through the Bun entrypoint and rejects operations after disposal", () => { const paths: string[] = []; const server = Bun.serve({ hostname: "127.0.0.1", @@ -12,19 +13,27 @@ describe("Bun client lifecycle", () => { return Response.json([]); }, }); - const client = await createApiClient({ - baseUrl: server.url.toString(), - accessToken: "test-token", - }); - try { - await expect(client.v1.listAllProjects()).resolves.toEqual([]); - expect(paths).toEqual(["/v1/projects"]); - await client.dispose(); - await expect(client.v1.listAllProjects()).rejects.toThrow(); - expect(paths).toEqual(["/v1/projects"]); - } finally { - await client.dispose(); - await server.stop(true); - } + return Effect.runPromise( + Effect.acquireUseRelease( + Effect.promise(() => + createApiClient({ + baseUrl: server.url.toString(), + accessToken: "test-token", + }), + ), + (client) => + Effect.gen(function* () { + yield* Effect.promise(() => expect(client.v1.listAllProjects()).resolves.toEqual([])); + expect(paths).toEqual(["/v1/projects"]); + yield* Effect.promise(() => client.dispose()); + yield* Effect.promise(() => expect(client.v1.listAllProjects()).rejects.toThrow()); + expect(paths).toEqual(["/v1/projects"]); + }), + (client) => + Effect.promise(() => client.dispose()).pipe( + Effect.andThen(Effect.promise(() => server.stop(true))), + ), + ), + ); }); }); diff --git a/packages/api/src/bun.ts b/packages/api/src/bun.ts index 1b90590435..9b80608494 100644 --- a/packages/api/src/bun.ts +++ b/packages/api/src/bun.ts @@ -7,18 +7,15 @@ import { type SupabaseApiClientOptions, type SupabaseApiConfig } from "./interna import { makePromiseClient, type PromiseClient } from "./internal/promise-client.ts"; /** Creates a Promise API client backed by a managed Bun runtime. */ -export async function createApiClient( +export function createApiClient( config: SupabaseApiConfig = {}, options?: SupabaseApiClientOptions, ): Promise { const runtime = ManagedRuntime.make(Layer.mergeAll(BunServices.layer, FetchHttpClient.layer)); - try { - const effectClient = await runtime.runPromise(makeApiClient(config, options)); - return makePromiseClient(runtime, effectClient); - } catch (error) { - await runtime.dispose(); - throw error; - } + return runtime + .runPromise(makeApiClient(config, options)) + .then((effectClient) => makePromiseClient(runtime, effectClient)) + .catch((error: unknown) => runtime.dispose().then(() => Promise.reject(error))); } /** Promise API client whose runtime resources are released by `dispose`. */ diff --git a/packages/api/src/config/api-config.layer.unit.test.ts b/packages/api/src/config/api-config.layer.unit.test.ts index 0951f499c5..086497f06b 100644 --- a/packages/api/src/config/api-config.layer.unit.test.ts +++ b/packages/api/src/config/api-config.layer.unit.test.ts @@ -1,27 +1,29 @@ import { describe, expect, test } from "vitest"; -import { ConfigProvider, Effect, Option } from "effect"; +import { ConfigProvider, Effect, Layer, Option } from "effect"; import { apiConfigLayer, DEFAULT_SUPABASE_API_URL } from "./api-config.layer.ts"; import { ApiConfig } from "./api-config.service.ts"; describe("apiConfigLayer", () => { - test("defaults the API URL and reads the access token from config", async () => { - const config = await Effect.runPromise( + test("defaults the API URL and reads the access token from config", () => + Effect.runPromise( Effect.gen(function* () { - return yield* ApiConfig; - }).pipe( - Effect.provide(apiConfigLayer), - Effect.provide( - ConfigProvider.layer( - ConfigProvider.fromUnknown({ - SUPABASE_ACCESS_TOKEN: "env-token", - }), + const config = yield* Effect.service(ApiConfig).pipe( + Effect.provide( + apiConfigLayer.pipe( + Layer.provide( + ConfigProvider.layer( + ConfigProvider.fromUnknown({ + SUPABASE_ACCESS_TOKEN: "env-token", + }), + ), + ), + ), ), - ), - ), - ); + ); - expect(config.baseUrl).toBe(DEFAULT_SUPABASE_API_URL); - expect(Option.isSome(config.accessToken)).toBe(true); - }); + expect(config.baseUrl).toBe(DEFAULT_SUPABASE_API_URL); + expect(Option.isSome(config.accessToken)).toBe(true); + }), + )); }); diff --git a/packages/api/src/effect.unit.test.ts b/packages/api/src/effect.unit.test.ts index 93249f8096..61098ed1c4 100644 --- a/packages/api/src/effect.unit.test.ts +++ b/packages/api/src/effect.unit.test.ts @@ -91,10 +91,10 @@ describe("SSO provider contracts", () => { test("decodes SSO provider responses without nested SAML and domain IDs", () => { expect(() => - Schema.decodeUnknownSync(V1ListAllSsoProviderOutput)({ items: [SPARSE_PROVIDER] }), + Schema.decodeSync(V1ListAllSsoProviderOutput)({ items: [SPARSE_PROVIDER] }), ).not.toThrow(); for (const schema of SINGLE_PROVIDER_SCHEMAS) { - expect(() => Schema.decodeUnknownSync(schema)(SPARSE_PROVIDER)).not.toThrow(); + expect(() => Schema.decodeSync(schema)(SPARSE_PROVIDER)).not.toThrow(); } }); @@ -106,7 +106,7 @@ describe("SSO provider contracts", () => { }; for (const schema of SINGLE_PROVIDER_SCHEMAS) { - const decoded = Schema.decodeUnknownSync(schema)(withLegacyIds); + const decoded = Schema.decodeSync(schema)(withLegacyIds); expect(decoded.saml).not.toHaveProperty("id"); expect(decoded.domains?.[0]).not.toHaveProperty("id"); } @@ -114,7 +114,7 @@ describe("SSO provider contracts", () => { test("accepts object-valued SSO attribute mapping defaults", () => { expect(() => - Schema.decodeUnknownSync(V1CreateASsoProviderInput)({ + Schema.decodeSync(V1CreateASsoProviderInput)({ ref: "abcdefghijklmnopqrst", type: "saml", attribute_mapping: { @@ -130,7 +130,7 @@ describe("SSO provider contracts", () => { describe("database OpenAPI response contract", () => { test("accepts a normal non-empty OpenAPI document", () => { expect(() => - Schema.decodeUnknownSync(V1GetDatabaseOpenapiOutput)({ + Schema.decodeSync(V1GetDatabaseOpenapiOutput)({ openapi: "3.0.0", info: { title: "Example", version: "1.0.0" }, paths: { @@ -146,257 +146,71 @@ describe("database OpenAPI response contract", () => { }); describe("makeApiClient", () => { - test("allows raw operations to override generated request headers", async () => { - let accept: string | undefined; - - const client = await Effect.runPromise( - makeApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - accept = request.headers.accept; - return Effect.succeed(jsonResponse(request, 200, {})); - }), - ), - ), - ); - - await Effect.runPromise( - client.executeRaw( - operationDefinitions.v1GetAFunctionBody, - { - ref: "abcdefghijklmnopqrst", - function_slug: "hello-world", - }, - { headers: { Accept: "multipart/form-data" } }, - ), - ); - - expect(accept).toBe("multipart/form-data"); - }); + test("allows raw operations to override generated request headers", () => + Effect.runPromise( + Effect.gen(function* () { + let accept: string | undefined; + + const client = yield* makeApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + accept = request.headers.accept; + return Effect.succeed(jsonResponse(request, 200, {})); + }), + ), + ); - test("forwards per-request options through generated typed methods", async () => { - let seen: { url: string; authorization: string | undefined } | undefined; - - const client = await Effect.runPromise( - makeApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - seen = { url: request.url, authorization: request.headers.authorization }; - return Effect.succeed(jsonResponse(request, 200, [])); - }), - ), - ), - ); - - await Effect.runPromise( - client.v1.listAllProjects( - {}, - { baseUrl: "https://api.example.test", accessToken: "per-request-token" }, - ), - ); - - expect(seen).toEqual({ - url: "https://api.example.test/v1/projects", - authorization: "Bearer per-request-token", - }); - }); + yield* client.executeRaw( + operationDefinitions.v1GetAFunctionBody, + { + ref: "abcdefghijklmnopqrst", + function_slug: "hello-world", + }, + { headers: { Accept: "multipart/form-data" } }, + ); - test("uses the default API URL when baseUrl is omitted", async () => { - const seenRequests: Array<{ method: string; url: string }> = []; - - const client = await Effect.runPromise( - makeApiClient({ accessToken: "test-token" }).pipe( - Effect.provide( - httpClientLayer((request) => { - seenRequests.push({ - method: request.method, - url: request.url, - }); - return Effect.succeed( - jsonResponse(request, 200, { - id: "project-id", - ref: "abcdefghijklmnopqrst", - organization_id: "org-id", - organization_slug: "my-org", - name: "project-name", - region: "us-east-1", - created_at: "2026-03-13T12:00:00.000Z", - status: "ACTIVE_HEALTHY", - database: { - host: "db.supabase.internal", - version: "17.0.1", - postgres_engine: "17", - release_channel: "ga", - }, - }), - ); - }), - ), - ), - ); - - await Effect.runPromise( - client.v1.getProject({ - ref: "abcdefghijklmnopqrst", + expect(accept).toBe("multipart/form-data"); }), - ); - - expect(seenRequests).toEqual([ - { - method: "GET", - url: "https://api.supabase.com/v1/projects/abcdefghijklmnopqrst", - }, - ]); - }); - - test("reads the access token from the environment when omitted", async () => { - const seenRequests: Array<{ authorization: string | undefined }> = []; - - const client = await Effect.runPromise( - makeApiClient().pipe( - Effect.provide( - httpClientLayer((request) => { - seenRequests.push({ - authorization: request.headers.authorization, - }); - return Effect.succeed( - jsonResponse(request, 200, { - id: "project-id", - ref: "abcdefghijklmnopqrst", - organization_id: "org-id", - organization_slug: "my-org", - name: "project-name", - region: "us-east-1", - created_at: "2026-03-13T12:00:00.000Z", - status: "ACTIVE_HEALTHY", - database: { - host: "db.supabase.internal", - version: "17.0.1", - postgres_engine: "17", - release_channel: "ga", - }, - }), - ); - }), - ), - Effect.provide( - ConfigProvider.layer( - ConfigProvider.fromUnknown({ - SUPABASE_ACCESS_TOKEN: "env-token", + )); + + test("forwards per-request options through generated typed methods", () => + Effect.runPromise( + Effect.gen(function* () { + let seen: { url: string; authorization: string | undefined } | undefined; + + const client = yield* makeApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + seen = { url: request.url, authorization: request.headers.authorization }; + return Effect.succeed(jsonResponse(request, 200, [])); }), ), - ), - ), - ); + ); - await Effect.runPromise( - client.v1.getProject({ - ref: "abcdefghijklmnopqrst", - }), - ); + yield* client.v1.listAllProjects( + {}, + { baseUrl: "https://api.example.test", accessToken: "per-request-token" }, + ); - expect(seenRequests).toEqual([{ authorization: "Bearer env-token" }]); - }); - - test("passes configured default headers through the facade client", async () => { - const seenRequests: Array<{ - command: string | undefined; - commandRunId: string | undefined; - authorization: string | undefined; - }> = []; - - const client = await Effect.runPromise( - makeApiClient({ - ...config, - headers: { - "X-Supabase-Command": "projects get", - "X-Supabase-Command-Run-ID": "run-456", - }, - }).pipe( - Effect.provide( - httpClientLayer((request) => { - seenRequests.push({ - command: request.headers["x-supabase-command"], - commandRunId: request.headers["x-supabase-command-run-id"], - authorization: request.headers.authorization, - }); - return Effect.succeed( - jsonResponse(request, 200, { - id: "project-id", - ref: "abcdefghijklmnopqrst", - organization_id: "org-id", - organization_slug: "my-org", - name: "project-name", - region: "us-east-1", - created_at: "2026-03-13T12:00:00.000Z", - status: "ACTIVE_HEALTHY", - database: { - host: "db.supabase.internal", - version: "17.0.1", - postgres_engine: "17", - release_channel: "ga", - }, - }), - ); - }), - ), - ), - ); - - await Effect.runPromise( - client.v1.getProject({ - ref: "abcdefghijklmnopqrst", + expect(seen).toEqual({ + url: "https://api.example.test/v1/projects", + authorization: "Bearer per-request-token", + }); }), - ); - - expect(seenRequests).toEqual([ - { - command: "projects get", - commandRunId: "run-456", - authorization: "Bearer test-token", - }, - ]); - }); - - test("fails early when no access token is configured", async () => { - const exit = await Effect.runPromise( - makeApiClient().pipe( - Effect.exit, - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - jsonResponse(request, 200, { - ok: true, - }), - ), - ), - ), - Effect.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({}))), - ), - ); - - expect(exit._tag).toBe("Failure"); - if (exit._tag === "Failure") { - expect(String(exit.cause)).toContain("Missing access token"); - } - }); - - test("returns only versioned methods under the v1 namespace", async () => { - const seenRequests: Array<{ method: string; url: string }> = []; - - const client = await Effect.runPromise( - makeApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - seenRequests.push({ - method: request.method, - url: request.url, - }); - - if ( - request.method === "POST" && - request.url === "https://api.supabase.com/v1/projects" - ) { + )); + + test("uses the default API URL when baseUrl is omitted", () => + Effect.runPromise( + Effect.gen(function* () { + const seenRequests: Array<{ method: string; url: string }> = []; + + const client = yield* makeApiClient({ accessToken: "test-token" }).pipe( + Effect.provide( + httpClientLayer((request) => { + seenRequests.push({ + method: request.method, + url: request.url, + }); return Effect.succeed( jsonResponse(request, 200, { id: "project-id", @@ -407,17 +221,45 @@ describe("makeApiClient", () => { region: "us-east-1", created_at: "2026-03-13T12:00:00.000Z", status: "ACTIVE_HEALTHY", + database: { + host: "db.supabase.internal", + version: "17.0.1", + postgres_engine: "17", + release_channel: "ga", + }, }), ); - } + }), + ), + ); - if ( - request.method === "GET" && - request.url === "https://api.supabase.com/v1/projects" - ) { - return Effect.succeed( - jsonResponse(request, 200, [ - { + yield* client.v1.getProject({ + ref: "abcdefghijklmnopqrst", + }); + + expect(seenRequests).toEqual([ + { + method: "GET", + url: "https://api.supabase.com/v1/projects/abcdefghijklmnopqrst", + }, + ]); + }), + )); + + test("reads the access token from the environment when omitted", () => + Effect.runPromise( + Effect.gen(function* () { + const seenRequests: Array<{ authorization: string | undefined }> = []; + + const client = yield* makeApiClient().pipe( + Effect.provide( + Layer.mergeAll( + httpClientLayer((request) => { + seenRequests.push({ + authorization: request.headers.authorization, + }); + return Effect.succeed( + jsonResponse(request, 200, { id: "project-id", ref: "abcdefghijklmnopqrst", organization_id: "org-id", @@ -432,185 +274,336 @@ describe("makeApiClient", () => { postgres_engine: "17", release_channel: "ga", }, - }, - ]), - ); - } - - return Effect.succeed( - jsonResponse(request, 200, { - id: "project-id", - ref: "abcdefghijklmnopqrst", - organization_id: "org-id", - organization_slug: "my-org", - name: "project-name", - region: "us-east-1", - created_at: "2026-03-13T12:00:00.000Z", - status: "ACTIVE_HEALTHY", - database: { - host: "db.supabase.internal", - version: "17.0.1", - postgres_engine: "17", - release_channel: "ga", - }, + }), + ); }), - ); - }), - ), - ), - ); - - expect("createAProject" in client).toBe(false); - expect("getProject" in client).toBe(false); - expect("listAllProjects" in client).toBe(false); - expect(typeof client.v1.createAProject).toBe("function"); - expect(typeof client.v1.getProject).toBe("function"); - expect(typeof client.v1.listAllProjects).toBe("function"); - - const created = await Effect.runPromise( - client.v1.createAProject({ - db_pass: "hunter2", - name: "project-name", - organization_slug: "my-org", - }), - ); - const project = await Effect.runPromise( - client.v1.getProject({ - ref: "abcdefghijklmnopqrst", + ConfigProvider.layer( + ConfigProvider.fromUnknown({ + SUPABASE_ACCESS_TOKEN: "env-token", + }), + ), + ), + ), + ); + + yield* client.v1.getProject({ + ref: "abcdefghijklmnopqrst", + }); + + expect(seenRequests).toEqual([{ authorization: "Bearer env-token" }]); }), - ); - const projects = await Effect.runPromise(client.v1.listAllProjects()); + )); + + test("passes configured default headers through the facade client", () => + Effect.runPromise( + Effect.gen(function* () { + const seenRequests: Array<{ + command: string | undefined; + commandRunId: string | undefined; + authorization: string | undefined; + }> = []; + + const client = yield* makeApiClient({ + ...config, + headers: { + "X-Supabase-Command": "projects get", + "X-Supabase-Command-Run-ID": "run-456", + }, + }).pipe( + Effect.provide( + httpClientLayer((request) => { + seenRequests.push({ + command: request.headers["x-supabase-command"], + commandRunId: request.headers["x-supabase-command-run-id"], + authorization: request.headers.authorization, + }); + return Effect.succeed( + jsonResponse(request, 200, { + id: "project-id", + ref: "abcdefghijklmnopqrst", + organization_id: "org-id", + organization_slug: "my-org", + name: "project-name", + region: "us-east-1", + created_at: "2026-03-13T12:00:00.000Z", + status: "ACTIVE_HEALTHY", + database: { + host: "db.supabase.internal", + version: "17.0.1", + postgres_engine: "17", + release_channel: "ga", + }, + }), + ); + }), + ), + ); - expect(created.ref).toBe("abcdefghijklmnopqrst"); - expect(project.database.host).toBe("db.supabase.internal"); - expect(projects).toHaveLength(1); - expect(seenRequests).toEqual([ - { - method: "POST", - url: "https://api.supabase.com/v1/projects", - }, - { - method: "GET", - url: "https://api.supabase.com/v1/projects/abcdefghijklmnopqrst", - }, - { - method: "GET", - url: "https://api.supabase.com/v1/projects", - }, - ]); - }); + yield* client.v1.getProject({ + ref: "abcdefghijklmnopqrst", + }); - test("addresses same-named v1 and v2 operations independently by namespace", async () => { - const seenRequests: Array<{ method: string; url: string }> = []; + expect(seenRequests).toEqual([ + { + command: "projects get", + commandRunId: "run-456", + authorization: "Bearer test-token", + }, + ]); + }), + )); + + test("fails early when no access token is configured", () => + Effect.runPromise( + Effect.gen(function* () { + const exit = yield* makeApiClient().pipe( + Effect.exit, + Effect.provide( + Layer.mergeAll( + httpClientLayer((request) => + Effect.succeed( + jsonResponse(request, 200, { + ok: true, + }), + ), + ), + ConfigProvider.layer(ConfigProvider.fromUnknown({})), + ), + ), + ); - const client = await Effect.runPromise( - makeApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - seenRequests.push({ - method: request.method, - url: request.url, - }); + expect(exit._tag).toBe("Failure"); + if (exit._tag === "Failure") { + expect(String(exit.cause)).toContain("Missing access token"); + } + }), + )); + + test("returns only versioned methods under the v1 namespace", () => + Effect.runPromise( + Effect.gen(function* () { + const seenRequests: Array<{ method: string; url: string }> = []; + + const client = yield* makeApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + seenRequests.push({ + method: request.method, + url: request.url, + }); + + if ( + request.method === "POST" && + request.url === "https://api.supabase.com/v1/projects" + ) { + return Effect.succeed( + jsonResponse(request, 200, { + id: "project-id", + ref: "abcdefghijklmnopqrst", + organization_id: "org-id", + organization_slug: "my-org", + name: "project-name", + region: "us-east-1", + created_at: "2026-03-13T12:00:00.000Z", + status: "ACTIVE_HEALTHY", + }), + ); + } + + if ( + request.method === "GET" && + request.url === "https://api.supabase.com/v1/projects" + ) { + return Effect.succeed( + jsonResponse(request, 200, [ + { + id: "project-id", + ref: "abcdefghijklmnopqrst", + organization_id: "org-id", + organization_slug: "my-org", + name: "project-name", + region: "us-east-1", + created_at: "2026-03-13T12:00:00.000Z", + status: "ACTIVE_HEALTHY", + database: { + host: "db.supabase.internal", + version: "17.0.1", + postgres_engine: "17", + release_channel: "ga", + }, + }, + ]), + ); + } - if (request.url === "https://api.supabase.com/v1/organizations/my-org/members") { return Effect.succeed( - jsonResponse(request, 200, [ - { - user_id: "user-id", - user_name: "user-name", - role_name: "Owner", - mfa_enabled: false, - avatar_url: null, + jsonResponse(request, 200, { + id: "project-id", + ref: "abcdefghijklmnopqrst", + organization_id: "org-id", + organization_slug: "my-org", + name: "project-name", + region: "us-east-1", + created_at: "2026-03-13T12:00:00.000Z", + status: "ACTIVE_HEALTHY", + database: { + host: "db.supabase.internal", + version: "17.0.1", + postgres_engine: "17", + release_channel: "ga", }, - ]), + }), ); - } + }), + ), + ); + + expect("createAProject" in client).toBe(false); + expect("getProject" in client).toBe(false); + expect("listAllProjects" in client).toBe(false); + expect(typeof client.v1.createAProject).toBe("function"); + expect(typeof client.v1.getProject).toBe("function"); + expect(typeof client.v1.listAllProjects).toBe("function"); + + const created = yield* client.v1.createAProject({ + db_pass: "hunter2", + name: "project-name", + organization_slug: "my-org", + }); + const project = yield* client.v1.getProject({ + ref: "abcdefghijklmnopqrst", + }); + const projects = yield* client.v1.listAllProjects(); + + expect(created.ref).toBe("abcdefghijklmnopqrst"); + expect(project.database.host).toBe("db.supabase.internal"); + expect(projects).toHaveLength(1); + expect(seenRequests).toEqual([ + { + method: "POST", + url: "https://api.supabase.com/v1/projects", + }, + { + method: "GET", + url: "https://api.supabase.com/v1/projects/abcdefghijklmnopqrst", + }, + { + method: "GET", + url: "https://api.supabase.com/v1/projects", + }, + ]); + }), + )); + + test("addresses same-named v1 and v2 operations independently by namespace", () => + Effect.runPromise( + Effect.gen(function* () { + const seenRequests: Array<{ method: string; url: string }> = []; + + const client = yield* makeApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + seenRequests.push({ + method: request.method, + url: request.url, + }); + + if (request.url === "https://api.supabase.com/v1/organizations/my-org/members") { + return Effect.succeed( + jsonResponse(request, 200, [ + { + user_id: "user-id", + user_name: "user-name", + role_name: "Owner", + mfa_enabled: false, + avatar_url: null, + }, + ]), + ); + } - return Effect.succeed( - jsonResponse(request, 200, { - data: [], - links: { prev: null, next: null }, - }), - ); - }), - ), - ), - ); - - expect(typeof client.v1.listOrganizationMembers).toBe("function"); - expect(typeof client.v2.listOrganizationMembers).toBe("function"); - - const v1Members = await Effect.runPromise( - client.v1.listOrganizationMembers({ slug: "my-org" }), - ); - const v2Members = await Effect.runPromise( - client.v2.listOrganizationMembers({ slug: "my-org" }), - ); - - expect(v1Members).toEqual([ - { - user_id: "user-id", - user_name: "user-name", - role_name: "Owner", - mfa_enabled: false, - avatar_url: null, - }, - ]); - expect(v2Members.data).toEqual([]); - expect(seenRequests).toEqual([ - { - method: "GET", - url: "https://api.supabase.com/v1/organizations/my-org/members", - }, - { - method: "GET", - url: "https://api.supabase.com/v2/organizations/my-org/members", - }, - ]); - }); + return Effect.succeed( + jsonResponse(request, 200, { + data: [], + links: { prev: null, next: null }, + }), + ); + }), + ), + ); - test("serializes generated binary methods through the effect facade", async () => { - let seenRequest: HttpClientRequest.HttpClientRequest | undefined; - - const client = await Effect.runPromise( - makeApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - seenRequest = request; - return Effect.succeed( - jsonResponse(request, 201, { - id: "function-id", - slug: "demo", - name: "Demo Function", - status: "ACTIVE", - version: 1, - created_at: 1_710_000_000, - updated_at: 1_710_000_001, - verify_jwt: true, - entrypoint_path: "functions/demo/index.ts", - import_map_path: "functions/demo/deno.json", - ezbr_sha256: "abc123", - }), - ); - }), - ), - ), - ); - - const body = new Blob(["console.log('blob body');"]); - const result = await Effect.runPromise( - client.v1.createAFunction({ - ref: "abcdefghijklmnopqrst", - slug: "demo", - verify_jwt: true, - entrypoint_path: "functions/demo/index.ts", - body, + expect(typeof client.v1.listOrganizationMembers).toBe("function"); + expect(typeof client.v2.listOrganizationMembers).toBe("function"); + + const v1Members = yield* client.v1.listOrganizationMembers({ slug: "my-org" }); + const v2Members = yield* client.v2.listOrganizationMembers({ slug: "my-org" }); + + expect(v1Members).toEqual([ + { + user_id: "user-id", + user_name: "user-name", + role_name: "Owner", + mfa_enabled: false, + avatar_url: null, + }, + ]); + expect(v2Members.data).toEqual([]); + expect(seenRequests).toEqual([ + { + method: "GET", + url: "https://api.supabase.com/v1/organizations/my-org/members", + }, + { + method: "GET", + url: "https://api.supabase.com/v2/organizations/my-org/members", + }, + ]); }), - ); + )); - expect(result.slug).toBe("demo"); - expect(seenRequest?.headers["content-type"]).toBe("application/vnd.denoland.eszip"); - expect(new URL(seenRequest!.url).pathname).toBe("/v1/projects/abcdefghijklmnopqrst/functions"); - expect(requestBodyText(seenRequest!)).toBe("console.log('blob body');"); - }); + test("serializes generated binary methods through the effect facade", () => + Effect.runPromise( + Effect.gen(function* () { + let seenRequest: HttpClientRequest.HttpClientRequest | undefined; + + const client = yield* makeApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + seenRequest = request; + return Effect.succeed( + jsonResponse(request, 201, { + id: "function-id", + slug: "demo", + name: "Demo Function", + status: "ACTIVE", + version: 1, + created_at: 1_710_000_000, + updated_at: 1_710_000_001, + verify_jwt: true, + entrypoint_path: "functions/demo/index.ts", + import_map_path: "functions/demo/deno.json", + ezbr_sha256: "abc123", + }), + ); + }), + ), + ); + + const body = new Blob(["console.log('blob body');"]); + const result = yield* client.v1.createAFunction({ + ref: "abcdefghijklmnopqrst", + slug: "demo", + verify_jwt: true, + entrypoint_path: "functions/demo/index.ts", + body, + }); + + expect(result.slug).toBe("demo"); + expect(seenRequest?.headers["content-type"]).toBe("application/vnd.denoland.eszip"); + expect(new URL(seenRequest!.url).pathname).toBe( + "/v1/projects/abcdefghijklmnopqrst/functions", + ); + expect(requestBodyText(seenRequest!)).toBe("console.log('blob body');"); + }), + )); }); diff --git a/packages/api/src/entrypoints.unit.test.ts b/packages/api/src/entrypoints.unit.test.ts index ca5dadfcac..6519dadfed 100644 --- a/packages/api/src/entrypoints.unit.test.ts +++ b/packages/api/src/entrypoints.unit.test.ts @@ -1,7 +1,7 @@ -import { existsSync, readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { NodeServices } from "@effect/platform-node"; +import { Effect, FileSystem, Path, Schema } from "effect"; import { describe, expect, test } from "vitest"; import * as effectModule from "./effect.ts"; @@ -26,32 +26,45 @@ describe("@supabase/api entrypoints", () => { expect(effectModule.DEFAULT_SUPABASE_API_URL).toBe("https://api.supabase.com"); }); - test("does not generate separate promise or standalone operation artifacts", () => { - const srcDir = dirname(fileURLToPath(import.meta.url)); - expect(existsSync(join(srcDir, "generated/promise-client.ts"))).toBe(false); - expect(existsSync(join(srcDir, "generated/effect-operations.ts"))).toBe(false); - }); + test("does not generate separate promise or standalone operation artifacts", () => + Effect.runPromise( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const srcDir = path.dirname(fileURLToPath(import.meta.url)); + expect(yield* fs.exists(path.join(srcDir, "generated/promise-client.ts"))).toBe(false); + expect(yield* fs.exists(path.join(srcDir, "generated/effect-operations.ts"))).toBe(false); + }).pipe(Effect.provide(NodeServices.layer)), + )); - test("ships the OpenAPI spec as a json subpath artifact", () => { - const srcDir = dirname(fileURLToPath(import.meta.url)); - const packageJson = JSON.parse(readFileSync(join(srcDir, "../package.json"), "utf8")) as { - readonly exports: Record>; - }; - const openApiDocument = JSON.parse( - readFileSync(join(srcDir, "generated/openapi.json"), "utf8"), - ) as { readonly openapi: string }; + test("ships the OpenAPI spec as a json subpath artifact", () => + Effect.runPromise( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const decoder = new TextDecoder("utf-8", { ignoreBOM: true }); + const srcDir = path.dirname(fileURLToPath(import.meta.url)); + const packageJson = yield* Schema.decodeEffect( + Schema.fromJsonString( + Schema.Struct({ exports: Schema.Record(Schema.String, Schema.Unknown) }), + ), + )(decoder.decode(yield* fs.readFile(path.join(srcDir, "../package.json")))); + const openApiDocument = yield* Schema.decodeEffect( + Schema.fromJsonString(Schema.Struct({ openapi: Schema.Unknown })), + )(decoder.decode(yield* fs.readFile(path.join(srcDir, "generated/openapi.json")))); - expect(packageJson.exports["."]).toEqual({ - bun: "./src/bun.ts", - default: "./src/node.ts", - }); - expect(packageJson.exports["./effect"]).toBe("./src/effect.ts"); - expect(packageJson.exports["./permissions"]).toBe("./src/generated/permissions.ts"); - expect(packageJson.exports["./openapi.json"]).toBe("./src/generated/openapi.json"); - expect(packageJson.exports["./bun"]).toBeUndefined(); - expect(packageJson.exports["./node"]).toBeUndefined(); - expect(openApiDocument.openapi).toBe("3.0.0"); - }); + expect(packageJson.exports["."]).toEqual({ + bun: "./src/bun.ts", + default: "./src/node.ts", + }); + expect(packageJson.exports["./effect"]).toBe("./src/effect.ts"); + expect(packageJson.exports["./permissions"]).toBe("./src/generated/permissions.ts"); + expect(packageJson.exports["./openapi.json"]).toBe("./src/generated/openapi.json"); + expect(packageJson.exports["./bun"]).toBeUndefined(); + expect(packageJson.exports["./node"]).toBeUndefined(); + expect(openApiDocument.openapi).toBe("3.0.0"); + }).pipe(Effect.provide(NodeServices.layer)), + )); test("exports a stable raw OpenAPI operation id map", () => { expect(Object.keys(effectModule.openApiOperationIdMap)).toHaveLength( diff --git a/packages/api/src/generated-contract-sync.unit.test.ts b/packages/api/src/generated-contract-sync.unit.test.ts index b103cbdf2b..e5cc25efc1 100644 --- a/packages/api/src/generated-contract-sync.unit.test.ts +++ b/packages/api/src/generated-contract-sync.unit.test.ts @@ -1,7 +1,7 @@ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { NodeServices } from "@effect/platform-node"; +import { Effect, FileSystem, Schema } from "effect"; import { describe, expect, test } from "vitest"; import { openApiOperationIdMap, operationDefinitions } from "./generated/contracts.ts"; @@ -25,8 +25,12 @@ interface SnapshotOperation { readonly operationId: string; } -const openApiJsonPath = join(dirname(fileURLToPath(import.meta.url)), "generated/openapi.json"); -const rawOpenApiJson = readFileSync(openApiJsonPath, "utf8"); +const openApiJsonPath = fileURLToPath(new URL("./generated/openapi.json", import.meta.url)); +const readOpenApiJson = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + return new TextDecoder("utf-8", { ignoreBOM: true }).decode(yield* fs.readFile(openApiJsonPath)); +}).pipe(Effect.provide(NodeServices.layer)); +const rawOpenApiJson = await Effect.runPromise(readOpenApiJson); const openApiDocument = JSON.parse(rawOpenApiJson) as OpenApiDocumentShape; function extractSnapshotOperations( @@ -173,10 +177,15 @@ describe("generated client drift against the committed openapi.json snapshot", ( // committed file doesn't hold, since oxfmt collapses short arrays onto one line // after generation. This instead checks that the bytes parse deterministically // and keep a single trailing newline. - test("parses the committed snapshot deterministically and keeps a single trailing newline", () => { - const reparsed = JSON.parse(readFileSync(openApiJsonPath, "utf8")) as OpenApiDocumentShape; - expect(reparsed).toEqual(openApiDocument); - expect(rawOpenApiJson.endsWith("\n")).toBe(true); - expect(rawOpenApiJson.endsWith("\n\n")).toBe(false); - }); + test("parses the committed snapshot deterministically and keeps a single trailing newline", () => + Effect.runPromise( + Effect.gen(function* () { + const reparsed = yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))( + yield* readOpenApiJson, + ); + expect(reparsed).toEqual(openApiDocument); + expect(rawOpenApiJson.endsWith("\n")).toBe(true); + expect(rawOpenApiJson.endsWith("\n\n")).toBe(false); + }), + )); }); diff --git a/packages/api/src/internal/client.ts b/packages/api/src/internal/client.ts index 7a4d3a0855..48a5f4f0d7 100644 --- a/packages/api/src/internal/client.ts +++ b/packages/api/src/internal/client.ts @@ -1,5 +1,6 @@ import { Clock, Data, Duration, Effect, Option, Context, Random, Schedule } from "effect"; import * as Cause from "effect/Cause"; +import { NodeInspectSymbol } from "effect/Inspectable"; import * as Redacted from "effect/Redacted"; import type { SchemaError } from "effect/Schema"; import * as Schema from "effect/Schema"; @@ -97,12 +98,22 @@ export class SupabaseApiClient extends Context.Service { + override readonly name = "SupabaseApiConfigError"; + + static { + /* Keep native Error inspect output instead of Effect's toJSON view. */ + Object.defineProperty(this.prototype, NodeInspectSymbol, { value: undefined }); + } constructor(message: string) { - super(message); - this.name = "SupabaseApiConfigError"; + super({ message }); + } + + override toJSON(): unknown { + return { _tag: this._tag, name: this.name }; } } @@ -114,17 +125,32 @@ export type SupabaseApiInputErrorSource = "generated_client" | "user_input"; * bug; command boundaries can reclassify a confirmed user-derived request as * `user_input`. The original schema failure is preserved as `cause`. */ -export class SupabaseApiInputError extends Error { - readonly _tag = "SupabaseApiInputError"; +export class SupabaseApiInputError extends Data.TaggedError("SupabaseApiInputError")<{ + readonly message: string; + readonly cause?: unknown; +}> { + override readonly name = "SupabaseApiInputError"; #source: SupabaseApiInputErrorSource = "generated_client"; + static { + /* Keep native Error inspect output instead of Effect's toJSON view. */ + Object.defineProperty(this.prototype, NodeInspectSymbol, { value: undefined }); + } + get source(): SupabaseApiInputErrorSource { return this.#source; } constructor(message: string, options?: { readonly cause?: unknown }) { - super(message, options); - this.name = "SupabaseApiInputError"; + super( + options === undefined || !("cause" in options) + ? { message } + : { message, cause: options.cause }, + ); + } + + override toJSON(): unknown { + return { _tag: this._tag, name: this.name }; } static markAsUserInput(error: T): T { @@ -146,10 +172,8 @@ function resolveSupabaseApiConfig( const accessToken = config.accessToken ?? Option.getOrUndefined(apiConfig.accessToken); if (accessToken === undefined) { - return yield* Effect.fail( - new SupabaseApiConfigError( - "Missing access token. Provide `accessToken` or set `SUPABASE_ACCESS_TOKEN`.", - ), + return yield* new SupabaseApiConfigError( + "Missing access token. Provide `accessToken` or set `SUPABASE_ACCESS_TOKEN`.", ); } @@ -505,7 +529,7 @@ function asBinaryRequestBody(value: unknown): Effect.Effect new Uint8Array(await revealed.arrayBuffer()), + try: () => revealed.arrayBuffer().then((buffer) => new Uint8Array(buffer)), catch: (cause) => new HttpBody.HttpBodyError({ reason: { _tag: "JsonError" }, cause }), }); } diff --git a/packages/api/src/internal/client.unit.test.ts b/packages/api/src/internal/client.unit.test.ts index dbd83aa270..5ce9c56fe1 100644 --- a/packages/api/src/internal/client.unit.test.ts +++ b/packages/api/src/internal/client.unit.test.ts @@ -11,6 +11,7 @@ import { Random, Redacted, } from "effect"; +import { NodeInspectSymbol } from "effect/Inspectable"; import { TestClock } from "effect/testing"; import * as HttpBody from "effect/http/HttpBody"; import * as HttpClient from "effect/http/HttpClient"; @@ -27,6 +28,7 @@ import { type SupabaseApiClientOptions, type SupabaseApiClientShape, type SupabaseApiRetryOptions, + SupabaseApiConfigError, SupabaseApiInputError, } from "./client.ts"; @@ -136,15 +138,10 @@ function formDataTextValue(formData: FormData, key: string): string { return value; } -async function formDataFileTexts(formData: FormData, key: string): Promise> { +function formDataFileTexts(formData: FormData, key: string): Effect.Effect> { const values = formData.getAll(key); - return Promise.all( - values.map(async (value) => { - if (typeof value === "string") { - return value; - } - return value.text(); - }), + return Effect.forEach(values, (value) => + typeof value === "string" ? Effect.succeed(value) : Effect.promise(() => value.text()), ); } @@ -178,15 +175,13 @@ const config = { function onVirtualClock( effect: Effect.Effect, jitter = 0.5, -): Promise { - return Effect.runPromise( - effect.pipe( - Effect.provideService(Random.Random, { - nextIntUnsafe: () => 0, - nextDoubleUnsafe: () => jitter, - }), - Effect.provide(TestClock.layer()), - ), +): Effect.Effect { + return effect.pipe( + Effect.provideService(Random.Random, { + nextIntUnsafe: () => 0, + nextDoubleUnsafe: () => jitter, + }), + Effect.provide(TestClock.layer()), ); } @@ -229,994 +224,1026 @@ const listProjects = (client: SupabaseApiClientShape) => client.execute<"v1ListAllProjects">(operationDefinitions.v1ListAllProjects, {}); describe("makeSupabaseApiClient", () => { - test("defaults request-schema failures to generated-client provenance", async () => { - let requests = 0; - const client = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - requests += 1; - return Effect.succeed(jsonResponse(request, 200, {})); - }), - ), - ), - ); - - const executeError = await Effect.runPromise( - client - .execute(operationDefinitions.v1DeleteAFunction, { - ref: "invalid-ref", - function_slug: "hello-world", - }) - .pipe(Effect.flip), - ); - const executeRawError = await Effect.runPromise( - client - .executeRaw(operationDefinitions.v1DeleteAFunction, { - ref: "invalid-ref", - function_slug: "hello-world", - }) - .pipe(Effect.flip), - ); - - for (const error of [executeError, executeRawError]) { - expect(error).toBeInstanceOf(SupabaseApiInputError); - if (!(error instanceof SupabaseApiInputError)) { - throw new Error("expected SupabaseApiInputError"); - } - expect(error.source).toBe("generated_client"); - } + test("defaults request-schema failures to generated-client provenance", () => + Effect.runPromise( + Effect.gen(function* () { + let requests = 0; + const client = yield* makeSupabaseApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + requests += 1; + return Effect.succeed(jsonResponse(request, 200, {})); + }), + ), + ); - if (!(executeRawError instanceof SupabaseApiInputError)) { - throw new Error("expected SupabaseApiInputError"); - } - expect(markSupabaseApiInputErrorAsUserInput(executeRawError)).toBe(executeRawError); - expect(executeRawError.source).toBe("user_input"); - expect(requests).toBe(0); - }); + const executeError = yield* client + .execute(operationDefinitions.v1DeleteAFunction, { + ref: "invalid-ref", + function_slug: "hello-world", + }) + .pipe(Effect.flip); + const executeRawError = yield* client + .executeRaw(operationDefinitions.v1DeleteAFunction, { + ref: "invalid-ref", + function_slug: "hello-world", + }) + .pipe(Effect.flip); + + for (const error of [executeError, executeRawError]) { + expect(error).toBeInstanceOf(SupabaseApiInputError); + if (!(error instanceof SupabaseApiInputError)) { + throw new Error("expected SupabaseApiInputError"); + } + expect(error.source).toBe("generated_client"); + } - test("rejects unknown request keys at any depth before sending a request", async () => { - let requests = 0; - const client = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - requests += 1; - return Effect.succeed(jsonResponse(request, 200, {})); - }), - ), - ), - ); - const ref = "abcdefghijklmnopqrst"; - const topLevelExcess = { ref, requestedConfig: { database: true }, junk: 1 }; - const nestedExcess = { ref, requestedConfig: { database: true, junk: 1 } }; + if (!(executeRawError instanceof SupabaseApiInputError)) { + throw new Error("expected SupabaseApiInputError"); + } + expect(markSupabaseApiInputErrorAsUserInput(executeRawError)).toBe(executeRawError); + expect(executeRawError.source).toBe("user_input"); + expect(requests).toBe(0); + }), + )); - const errors = await Promise.all([ - Effect.runPromise( - client - .execute(operationDefinitions.v1UpdateSslEnforcementConfig, topLevelExcess) - .pipe(Effect.flip), - ), - Effect.runPromise( - client - .executeRaw(operationDefinitions.v1UpdateSslEnforcementConfig, nestedExcess) - .pipe(Effect.flip), - ), - ]); + test("rejects unknown request keys at any depth before sending a request", () => + Effect.runPromise( + Effect.gen(function* () { + let requests = 0; + const client = yield* makeSupabaseApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + requests += 1; + return Effect.succeed(jsonResponse(request, 200, {})); + }), + ), + ); + const ref = "abcdefghijklmnopqrst"; + const topLevelExcess = { ref, requestedConfig: { database: true }, junk: 1 }; + const nestedExcess = { ref, requestedConfig: { database: true, junk: 1 } }; + + const errors = yield* Effect.all( + [ + client + .execute(operationDefinitions.v1UpdateSslEnforcementConfig, topLevelExcess) + .pipe(Effect.flip), + client + .executeRaw(operationDefinitions.v1UpdateSslEnforcementConfig, nestedExcess) + .pipe(Effect.flip), + ], + { concurrency: "unbounded" }, + ); - for (const error of errors) { - expect(error).toBeInstanceOf(SupabaseApiInputError); - } - expect(errors[0]?.message).toContain('["junk"]'); - expect(errors[1]?.message).toContain('["requestedConfig"]["junk"]'); - expect(requests).toBe(0); - }); + for (const error of errors) { + expect(error).toBeInstanceOf(SupabaseApiInputError); + } + expect(errors[0]?.message).toContain('["junk"]'); + expect(errors[1]?.message).toContain('["requestedConfig"]["junk"]'); + expect(requests).toBe(0); + }), + )); - test("per-request options override the base URL, token, and headers for that request only", async () => { - const seen: Array<{ - url: string; - authorization: string | undefined; - userAgent: string | undefined; - custom: string | undefined; - }> = []; - const client = await Effect.runPromise( - makeSupabaseApiClient({ ...config, headers: { "X-Custom": "default" } }).pipe( - Effect.provide( - httpClientLayer((request) => { - seen.push({ - url: request.url, - authorization: request.headers.authorization, - userAgent: request.headers["user-agent"], - custom: request.headers["x-custom"], - }); - return Effect.succeed(jsonResponse(request, 200, [])); - }), - ), - ), - ); + test("per-request options override the base URL, token, and headers for that request only", () => + Effect.runPromise( + Effect.gen(function* () { + const seen: Array<{ + url: string; + authorization: string | undefined; + userAgent: string | undefined; + custom: string | undefined; + }> = []; + const client = yield* makeSupabaseApiClient({ + ...config, + headers: { "X-Custom": "default" }, + }).pipe( + Effect.provide( + httpClientLayer((request) => { + seen.push({ + url: request.url, + authorization: request.headers.authorization, + userAgent: request.headers["user-agent"], + custom: request.headers["x-custom"], + }); + return Effect.succeed(jsonResponse(request, 200, [])); + }), + ), + ); - await Effect.runPromise( - client.execute( - operationDefinitions.v1ListAllProjects, - {}, - { - baseUrl: "https://api.example.test/prefix", - accessToken: Redacted.make("override-token"), - headers: { "X-Custom": "override" }, - }, - ), - ); - await Effect.runPromise( - client.executeRaw( - operationDefinitions.v1ListAllProjects, - {}, - { baseUrl: "https://raw.example.test", accessToken: "raw-token" }, - ), - ); - await Effect.runPromise(client.execute(operationDefinitions.v1ListAllProjects, {})); - - expect(seen).toEqual([ - { - url: "https://api.example.test/prefix/v1/projects", - authorization: "Bearer override-token", - userAgent: "supabase-api/test", - custom: "override", - }, - { - url: "https://raw.example.test/v1/projects", - authorization: "Bearer raw-token", - userAgent: "supabase-api/test", - custom: "default", - }, - { - url: "https://api.supabase.com/v1/projects", - authorization: "Bearer test-token", - userAgent: "supabase-api/test", - custom: "default", - }, - ]); - }); + yield* client.execute( + operationDefinitions.v1ListAllProjects, + {}, + { + baseUrl: "https://api.example.test/prefix", + accessToken: Redacted.make("override-token"), + headers: { "X-Custom": "override" }, + }, + ); + yield* client.executeRaw( + operationDefinitions.v1ListAllProjects, + {}, + { baseUrl: "https://raw.example.test", accessToken: "raw-token" }, + ); + yield* client.execute(operationDefinitions.v1ListAllProjects, {}); + + expect(seen).toEqual([ + { + url: "https://api.example.test/prefix/v1/projects", + authorization: "Bearer override-token", + userAgent: "supabase-api/test", + custom: "override", + }, + { + url: "https://raw.example.test/v1/projects", + authorization: "Bearer raw-token", + userAgent: "supabase-api/test", + custom: "default", + }, + { + url: "https://api.supabase.com/v1/projects", + authorization: "Bearer test-token", + userAgent: "supabase-api/test", + custom: "default", + }, + ]); + }), + )); - test("requests with per-request overrides still pass through transformClient", async () => { - const transformed: Array = []; - const wire: Array<{ url: string; stamp: string | undefined }> = []; - const client = await Effect.runPromise( - makeSupabaseApiClient(config, { - transformClient: (inner) => - Effect.succeed( - HttpClient.mapRequest(inner, (request) => { - transformed.push(request.url); - return HttpClientRequest.setHeader(request, "X-Stamp", "stamped"); + test("requests with per-request overrides still pass through transformClient", () => + Effect.runPromise( + Effect.gen(function* () { + const transformed: Array = []; + const wire: Array<{ url: string; stamp: string | undefined }> = []; + const client = yield* makeSupabaseApiClient(config, { + transformClient: (inner) => + Effect.succeed( + HttpClient.mapRequest(inner, (request) => { + transformed.push(request.url); + return HttpClientRequest.setHeader(request, "X-Stamp", "stamped"); + }), + ), + }).pipe( + Effect.provide( + httpClientLayer((request) => { + wire.push({ url: request.url, stamp: request.headers["x-stamp"] }); + return Effect.succeed(jsonResponse(request, 200, [])); }), ), - }).pipe( - Effect.provide( - httpClientLayer((request) => { - wire.push({ url: request.url, stamp: request.headers["x-stamp"] }); - return Effect.succeed(jsonResponse(request, 200, [])); - }), - ), - ), - ); + ); - await Effect.runPromise( - client.execute( - operationDefinitions.v1ListAllProjects, - {}, - { baseUrl: "https://api.example.test" }, - ), - ); + yield* client.execute( + operationDefinitions.v1ListAllProjects, + {}, + { baseUrl: "https://api.example.test" }, + ); - expect(transformed).toEqual(["https://api.example.test/v1/projects"]); - expect(wire).toEqual([{ url: "https://api.example.test/v1/projects", stamp: "stamped" }]); - }); + expect(transformed).toEqual(["https://api.example.test/v1/projects"]); + expect(wire).toEqual([{ url: "https://api.example.test/v1/projects", stamp: "stamped" }]); + }), + )); - test("fails request-body construction before sending a request", async () => { - class BrokenBlob extends Blob { - override arrayBuffer(): Promise { - return Promise.reject(new Error("body read failed")); - } - } + test("fails request-body construction before sending a request", () => + Effect.runPromise( + Effect.gen(function* () { + class BrokenBlob extends Blob { + override arrayBuffer(): Promise { + return Promise.reject(new Error("body read failed")); + } + } - let requests = 0; - const error = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.executeRaw(operationDefinitions.v1CreateAFunction, { - ref: "abcdefghijklmnopqrst", - slug: "demo", - body: new BrokenBlob([]), - }), - ), - Effect.provide( - httpClientLayer((request) => { - requests += 1; - return Effect.succeed(functionResponse(request, 201)); - }), - ), - Effect.flip, - ), - ); + let requests = 0; + const error = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.executeRaw(operationDefinitions.v1CreateAFunction, { + ref: "abcdefghijklmnopqrst", + slug: "demo", + body: new BrokenBlob([]), + }), + ), + Effect.provide( + httpClientLayer((request) => { + requests += 1; + return Effect.succeed(functionResponse(request, 201)); + }), + ), + Effect.flip, + ); - expect(error).toBeInstanceOf(HttpBody.HttpBodyError); - expect(requests).toBe(0); - }); + expect(error).toBeInstanceOf(HttpBody.HttpBodyError); + expect(requests).toBe(0); + }), + )); - test("executeRaw forwards extraBody fields and lets validated fields win", async () => { - const bodies: unknown[] = []; - await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.executeRaw( - operationDefinitions.v1UpdatePostgresConfig, - { ref: "abcdefghijklmnopqrst", effective_cache_size: "4GB" }, - { - extraBody: { - effective_cache_size: "1GB", - future_parameter: "on", + test("executeRaw forwards extraBody fields and lets validated fields win", () => + Effect.runPromise( + Effect.gen(function* () { + const bodies: unknown[] = []; + yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.executeRaw( + operationDefinitions.v1UpdatePostgresConfig, + { ref: "abcdefghijklmnopqrst", effective_cache_size: "4GB" }, + { + extraBody: { + effective_cache_size: "1GB", + future_parameter: "on", + }, }, - }, + ), ), - ), - Effect.provide( - httpClientLayer((request) => { - bodies.push(JSON.parse(requestBodyText(request))); - return Effect.succeed(jsonResponse(request, 200, {})); - }), - ), - ), - ); - - expect(bodies).toEqual([{ effective_cache_size: "4GB", future_parameter: "on" }]); - }); - - test("executeRaw rejects extraBody on operations without a JSON field body", async () => { - let requests = 0; - const exit = await Effect.runPromiseExit( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.executeRaw(operationDefinitions.v1ListAllProjects, {}, { extraBody: { a: 1 } }), - ), - Effect.provide( - httpClientLayer((request) => { - requests += 1; - return Effect.succeed(jsonResponse(request, 200, [])); - }), - ), - ), - ); + Effect.provide( + httpClientLayer((request) => { + bodies.push(JSON.parse(requestBodyText(request))); + return Effect.succeed(jsonResponse(request, 200, {})); + }), + ), + ); - expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true); - expect(requests).toBe(0); - }); + expect(bodies).toEqual([{ effective_cache_size: "4GB", future_parameter: "on" }]); + }), + )); - test("retries transport errors for GET requests", async () => { - const { requestTimes, result } = await onVirtualClock( + test("executeRaw rejects extraBody on operations without a JSON field body", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall(listProjects, (request, attempt) => - attempt < 3 ? transportError(request, "socket reset") : jsonResponse(request, 200, []), + let requests = 0; + const exit = yield* Effect.exit( + makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.executeRaw( + operationDefinitions.v1ListAllProjects, + {}, + { extraBody: { a: 1 } }, + ), + ), + Effect.provide( + httpClientLayer((request) => { + requests += 1; + return Effect.succeed(jsonResponse(request, 200, [])); + }), + ), + ), ); - yield* TestClock.adjust(Duration.minutes(1)); - return { requestTimes: call.requestTimes, result: yield* Fiber.join(call.fiber) }; - }), - ); - expect(requestTimes).toHaveLength(3); - expect(result).toEqual(Exit.succeed([])); - }); + expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true); + expect(requests).toBe(0); + }), + )); - test("reveals redacted auth tokens only at the transport boundary", async () => { - let authorizationHeader: string | undefined; - - const result = await Effect.runPromise( - makeSupabaseApiClient({ - ...config, - accessToken: Redacted.make("redacted-token"), - }).pipe( - Effect.flatMap((client) => - client.execute<"v1CreateAProject">(operationDefinitions.v1CreateAProject, { - db_pass: "hunter2", - name: "project-name", - organization_slug: "my-org", - }), - ), - Effect.provide( - httpClientLayer((request) => { - authorizationHeader = request.headers.authorization; - return Effect.succeed( - jsonResponse(request, 200, { - id: "project-id", - ref: "abcdefghijklmnopqrst", - organization_id: "org-id", - organization_slug: "my-org", - name: "project-name", - region: "us-east-1", - created_at: "2026-03-13T12:00:00.000Z", - status: "ACTIVE_HEALTHY", - }), + test("retries transport errors for GET requests", () => + Effect.runPromise( + Effect.gen(function* () { + const { requestTimes, result } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(listProjects, (request, attempt) => + attempt < 3 + ? transportError(request, "socket reset") + : jsonResponse(request, 200, []), ); + yield* TestClock.adjust(Duration.minutes(1)); + return { requestTimes: call.requestTimes, result: yield* Fiber.join(call.fiber) }; }), - ), - ), - ); + ); - expect(authorizationHeader).toBe("Bearer redacted-token"); - expect(result.ref).toBe("abcdefghijklmnopqrst"); - }); + expect(requestTimes).toHaveLength(3); + expect(result).toEqual(Exit.succeed([])); + }), + )); - test("applies default headers alongside auth, user agent, and request headers", async () => { - let seenHeaders: - | { - authorization: string | undefined; - userAgent: string | undefined; - command: string | undefined; - commandRunId: string | undefined; - idempotencyKey: string | undefined; - } - | undefined; - - await Effect.runPromise( - makeSupabaseApiClient({ - ...config, - headers: { - "X-Supabase-Command": "branches list", - "X-Supabase-Command-Run-ID": "run-123", - }, - }).pipe( - Effect.flatMap((client) => - client.execute<"v1ApplyAMigration">(operationDefinitions.v1ApplyAMigration, { - ref: "abcdefghijklmnopqrst", - query: "select 1", - name: "smoke_test", - "Idempotency-Key": "migration-123", - }), - ), - Effect.provide( - httpClientLayer((request) => { - seenHeaders = { - authorization: request.headers.authorization, - userAgent: request.headers["user-agent"], - command: request.headers["x-supabase-command"], - commandRunId: request.headers["x-supabase-command-run-id"], - idempotencyKey: request.headers["idempotency-key"], - }; - return Effect.succeed( - HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })), - ); - }), - ), - ), - ); + test("reveals redacted auth tokens only at the transport boundary", () => + Effect.runPromise( + Effect.gen(function* () { + let authorizationHeader: string | undefined; + + const result = yield* makeSupabaseApiClient({ + ...config, + accessToken: Redacted.make("redacted-token"), + }).pipe( + Effect.flatMap((client) => + client.execute<"v1CreateAProject">(operationDefinitions.v1CreateAProject, { + db_pass: "hunter2", + name: "project-name", + organization_slug: "my-org", + }), + ), + Effect.provide( + httpClientLayer((request) => { + authorizationHeader = request.headers.authorization; + return Effect.succeed( + jsonResponse(request, 200, { + id: "project-id", + ref: "abcdefghijklmnopqrst", + organization_id: "org-id", + organization_slug: "my-org", + name: "project-name", + region: "us-east-1", + created_at: "2026-03-13T12:00:00.000Z", + status: "ACTIVE_HEALTHY", + }), + ); + }), + ), + ); - expect(seenHeaders).toEqual({ - authorization: "Bearer test-token", - userAgent: "supabase-api/test", - command: "branches list", - commandRunId: "run-123", - idempotencyKey: "migration-123", - }); - }); + expect(authorizationHeader).toBe("Bearer redacted-token"); + expect(result.ref).toBe("abcdefghijklmnopqrst"); + }), + )); - test("decodes nullable JWT templates in API key responses", async () => { - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1GetProjectApiKeys">(operationDefinitions.v1GetProjectApiKeys, { - ref: "abcdefghijklmnopqrst", - reveal: true, - }), - ), - Effect.provide( - httpClientLayer((request) => { - const url = requestUrl(request); - expect(url.pathname).toBe("/v1/projects/abcdefghijklmnopqrst/api-keys"); - expect(requestUrlParam(request, "reveal")).toBe("true"); - return Effect.succeed( - jsonResponse(request, 200, [ - { - name: "anon", - type: "legacy", - api_key: "anon-key", - secret_jwt_template: null, - }, - { - name: "service_role", - type: "secret", - api_key: "service-role-key", - secret_jwt_template: { role: "service_role" }, - }, - ]), - ); - }), - ), - ), - ); + test("applies default headers alongside auth, user agent, and request headers", () => + Effect.runPromise( + Effect.gen(function* () { + let seenHeaders: + | { + authorization: string | undefined; + userAgent: string | undefined; + command: string | undefined; + commandRunId: string | undefined; + idempotencyKey: string | undefined; + } + | undefined; + + yield* makeSupabaseApiClient({ + ...config, + headers: { + "X-Supabase-Command": "branches list", + "X-Supabase-Command-Run-ID": "run-123", + }, + }).pipe( + Effect.flatMap((client) => + client.execute<"v1ApplyAMigration">(operationDefinitions.v1ApplyAMigration, { + ref: "abcdefghijklmnopqrst", + query: "select 1", + name: "smoke_test", + "Idempotency-Key": "migration-123", + }), + ), + Effect.provide( + httpClientLayer((request) => { + seenHeaders = { + authorization: request.headers.authorization, + userAgent: request.headers["user-agent"], + command: request.headers["x-supabase-command"], + commandRunId: request.headers["x-supabase-command-run-id"], + idempotencyKey: request.headers["idempotency-key"], + }; + return Effect.succeed( + HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })), + ); + }), + ), + ); - expect(result).toEqual([ - { - name: "anon", - type: "legacy", - api_key: "anon-key", - secret_jwt_template: null, - }, - { - name: "service_role", - type: "secret", - api_key: "service-role-key", - secret_jwt_template: { role: "service_role" }, - }, - ]); - }); + expect(seenHeaders).toEqual({ + authorization: "Bearer test-token", + userAgent: "supabase-api/test", + command: "branches list", + commandRunId: "run-123", + idempotencyKey: "migration-123", + }); + }), + )); + + test("decodes nullable JWT templates in API key responses", () => + Effect.runPromise( + Effect.gen(function* () { + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1GetProjectApiKeys">(operationDefinitions.v1GetProjectApiKeys, { + ref: "abcdefghijklmnopqrst", + reveal: true, + }), + ), + Effect.provide( + httpClientLayer((request) => { + const url = requestUrl(request); + expect(url.pathname).toBe("/v1/projects/abcdefghijklmnopqrst/api-keys"); + expect(requestUrlParam(request, "reveal")).toBe("true"); + return Effect.succeed( + jsonResponse(request, 200, [ + { + name: "anon", + type: "legacy", + api_key: "anon-key", + secret_jwt_template: null, + }, + { + name: "service_role", + type: "secret", + api_key: "service-role-key", + secret_jwt_template: { role: "service_role" }, + }, + ]), + ); + }), + ), + ); + + expect(result).toEqual([ + { + name: "anon", + type: "legacy", + api_key: "anon-key", + secret_jwt_template: null, + }, + { + name: "service_role", + type: "secret", + api_key: "service-role-key", + secret_jwt_template: { role: "service_role" }, + }, + ]); + }), + )); // Regression payloads: the spec's Z-anchored pattern used to reject these and // break `link` and `branches list` outright. - test("decodes timestamps with a numeric UTC offset", async () => { - const apiKeys = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1GetProjectApiKeys">(operationDefinitions.v1GetProjectApiKeys, { - ref: "abcdefghijklmnopqrst", - }), - ), - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - jsonResponse(request, 200, [ - { - name: "anon", - type: "legacy", - api_key: "anon-key", - inserted_at: "2026-05-01T08:00:00+00:00", - updated_at: "2026-05-01T08:00:00.123456+02:00", - }, - ]), + test("decodes timestamps with a numeric UTC offset", () => + Effect.runPromise( + Effect.gen(function* () { + const apiKeys = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1GetProjectApiKeys">(operationDefinitions.v1GetProjectApiKeys, { + ref: "abcdefghijklmnopqrst", + }), + ), + Effect.provide( + httpClientLayer((request) => + Effect.succeed( + jsonResponse(request, 200, [ + { + name: "anon", + type: "legacy", + api_key: "anon-key", + inserted_at: "2026-05-01T08:00:00+00:00", + updated_at: "2026-05-01T08:00:00.123456+02:00", + }, + ]), + ), ), ), - ), - ), - ); + ); - expect(apiKeys[0]?.inserted_at).toBe("2026-05-01T08:00:00+00:00"); - expect(apiKeys[0]?.updated_at).toBe("2026-05-01T08:00:00.123456+02:00"); + expect(apiKeys[0]?.inserted_at).toBe("2026-05-01T08:00:00+00:00"); + expect(apiKeys[0]?.updated_at).toBe("2026-05-01T08:00:00.123456+02:00"); - const branches = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1ListAllBranches">(operationDefinitions.v1ListAllBranches, { - ref: "abcdefghijklmnopqrst", - }), - ), - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - jsonResponse(request, 200, [ - { - id: "6f8f9d2c-1f43-4b8a-9d0e-3a2b1c4d5e6f", - name: "preview", - project_ref: "abcdefghijklmnopqrst", - parent_project_ref: "tsrqponmlkjihgfedcba", - is_default: false, - persistent: false, - status: "MIGRATIONS_PASSED", - with_data: false, - created_at: "2026-08-06T19:27:30.261795+00:00", - updated_at: "2026-08-06T19:27:30.261795+00:00", - }, - ]), + const branches = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1ListAllBranches">(operationDefinitions.v1ListAllBranches, { + ref: "abcdefghijklmnopqrst", + }), + ), + Effect.provide( + httpClientLayer((request) => + Effect.succeed( + jsonResponse(request, 200, [ + { + id: "6f8f9d2c-1f43-4b8a-9d0e-3a2b1c4d5e6f", + name: "preview", + project_ref: "abcdefghijklmnopqrst", + parent_project_ref: "tsrqponmlkjihgfedcba", + is_default: false, + persistent: false, + status: "MIGRATIONS_PASSED", + with_data: false, + created_at: "2026-08-06T19:27:30.261795+00:00", + updated_at: "2026-08-06T19:27:30.261795+00:00", + }, + ]), + ), ), ), - ), - ), - ); + ); - expect(branches[0]?.created_at).toBe("2026-08-06T19:27:30.261795+00:00"); - }); + expect(branches[0]?.created_at).toBe("2026-08-06T19:27:30.261795+00:00"); + }), + )); - test("accepts missing custom-hostname SSL validation records", async () => { - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1GetHostnameConfig">(operationDefinitions.v1GetHostnameConfig, { - ref: "abcdefghijklmnopqrst", - }), - ), - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - jsonResponse(request, 200, { - status: "2_initiated", - custom_hostname: "shop.acme.dev", - data: { - success: true, - errors: [], - messages: [], - result: { - id: "hostname-id", - hostname: "shop.acme.dev", - ssl: { - status: "pending_validation", - }, - ownership_verification: { - type: "txt", - name: "_cf-custom-hostname.shop.acme.dev", - value: "verification-token", + test("accepts missing custom-hostname SSL validation records", () => + Effect.runPromise( + Effect.gen(function* () { + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1GetHostnameConfig">(operationDefinitions.v1GetHostnameConfig, { + ref: "abcdefghijklmnopqrst", + }), + ), + Effect.provide( + httpClientLayer((request) => + Effect.succeed( + jsonResponse(request, 200, { + status: "2_initiated", + custom_hostname: "shop.acme.dev", + data: { + success: true, + errors: [], + messages: [], + result: { + id: "hostname-id", + hostname: "shop.acme.dev", + ssl: { + status: "pending_validation", + }, + ownership_verification: { + type: "txt", + name: "_cf-custom-hostname.shop.acme.dev", + value: "verification-token", + }, + custom_origin_server: "abcdefghijklmnopqrst.supabase.co", + status: "pending", }, - custom_origin_server: "abcdefghijklmnopqrst.supabase.co", - status: "pending", }, - }, - }), + }), + ), ), ), - ), - ), - ); + ); - expect(result.data.result.ssl?.validation_records).toBeUndefined(); - }); + expect(result.data.result.ssl?.validation_records).toBeUndefined(); + }), + )); - test("accepts missing custom-hostname ownership verification", async () => { - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1GetHostnameConfig">(operationDefinitions.v1GetHostnameConfig, { - ref: "abcdefghijklmnopqrst", - }), - ), - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - jsonResponse(request, 200, { - status: "4_origin_setup_completed", - custom_hostname: "shop.acme.dev", - data: { - success: true, - errors: [], - messages: [], - result: { - id: "hostname-id", - hostname: "shop.acme.dev", - ssl: { + test("accepts missing custom-hostname ownership verification", () => + Effect.runPromise( + Effect.gen(function* () { + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1GetHostnameConfig">(operationDefinitions.v1GetHostnameConfig, { + ref: "abcdefghijklmnopqrst", + }), + ), + Effect.provide( + httpClientLayer((request) => + Effect.succeed( + jsonResponse(request, 200, { + status: "4_origin_setup_completed", + custom_hostname: "shop.acme.dev", + data: { + success: true, + errors: [], + messages: [], + result: { + id: "hostname-id", + hostname: "shop.acme.dev", + ssl: { + status: "active", + }, + custom_origin_server: "abcdefghijklmnopqrst.supabase.co", status: "active", }, - custom_origin_server: "abcdefghijklmnopqrst.supabase.co", - status: "active", }, - }, - }), + }), + ), ), ), - ), - ), - ); + ); - expect(result.data.result.ownership_verification).toBeUndefined(); - expect(result.data.result.ssl?.validation_records).toBeUndefined(); - }); + expect(result.data.result.ownership_verification).toBeUndefined(); + expect(result.data.result.ssl?.validation_records).toBeUndefined(); + }), + )); - test("accepts processing custom-hostname responses without top-level status or hostname", async () => { - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1GetHostnameConfig">(operationDefinitions.v1GetHostnameConfig, { - ref: "abcdefghijklmnopqrst", - }), - ), - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - jsonResponse(request, 200, { - data: { - success: true, - errors: [], - messages: [], - result: { - id: "hostname-id", - hostname: "shop.acme.dev", - ssl: { - status: "initializing", - }, - ownership_verification: { - type: "txt", - name: "_cf-custom-hostname.shop.acme.dev", - value: "verification-token", + test("accepts processing custom-hostname responses without top-level status or hostname", () => + Effect.runPromise( + Effect.gen(function* () { + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1GetHostnameConfig">(operationDefinitions.v1GetHostnameConfig, { + ref: "abcdefghijklmnopqrst", + }), + ), + Effect.provide( + httpClientLayer((request) => + Effect.succeed( + jsonResponse(request, 200, { + data: { + success: true, + errors: [], + messages: [], + result: { + id: "hostname-id", + hostname: "shop.acme.dev", + ssl: { + status: "initializing", + }, + ownership_verification: { + type: "txt", + name: "_cf-custom-hostname.shop.acme.dev", + value: "verification-token", + }, + custom_origin_server: "abcdefghijklmnopqrst.supabase.co", + status: "pending", }, - custom_origin_server: "abcdefghijklmnopqrst.supabase.co", - status: "pending", }, - }, - }), + }), + ), ), ), - ), - ), - ); - - expect(result.status).toBeUndefined(); - expect(result.custom_hostname).toBeUndefined(); - expect(result.data.result.ssl?.validation_records).toBeUndefined(); - }); - - test("does not retry 5xx responses for POST requests", async () => { - let attempts = 0; + ); - const exit = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1CreateAProject">(operationDefinitions.v1CreateAProject, { - db_pass: "hunter2", - name: "project-name", - organization_slug: "my-org", - }), - ), - Effect.exit, - Effect.provide( - httpClientLayer((request) => { - attempts += 1; - return Effect.succeed( - jsonResponse(request, 500, { - error: "do not retry post", - }), - ); - }), - ), - ), - ); + expect(result.status).toBeUndefined(); + expect(result.custom_hostname).toBeUndefined(); + expect(result.data.result.ssl?.validation_records).toBeUndefined(); + }), + )); - expect(attempts).toBe(1); - expect(Exit.isFailure(exit)).toBe(true); - }); + test("does not retry 5xx responses for POST requests", () => + Effect.runPromise( + Effect.gen(function* () { + let attempts = 0; + + const exit = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1CreateAProject">(operationDefinitions.v1CreateAProject, { + db_pass: "hunter2", + name: "project-name", + organization_slug: "my-org", + }), + ), + Effect.exit, + Effect.provide( + httpClientLayer((request) => { + attempts += 1; + return Effect.succeed( + jsonResponse(request, 500, { + error: "do not retry post", + }), + ); + }), + ), + ); - test("does not retry transport errors for POST requests", async () => { - let attempts = 0; + expect(attempts).toBe(1); + expect(Exit.isFailure(exit)).toBe(true); + }), + )); - const exit = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1CreateAProject">(operationDefinitions.v1CreateAProject, { - db_pass: "hunter2", - name: "project-name", - organization_slug: "my-org", - }), - ), - Effect.exit, - Effect.provide( - httpClientLayer((request) => { - attempts += 1; - return attempts === 1 - ? Effect.fail(transportError(request, "connection reset")) - : Effect.succeed( - jsonResponse(request, 409, { - message: "project already exists", - }), - ); - }), - ), - ), - ); + test("does not retry transport errors for POST requests", () => + Effect.runPromise( + Effect.gen(function* () { + let attempts = 0; + + const exit = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1CreateAProject">(operationDefinitions.v1CreateAProject, { + db_pass: "hunter2", + name: "project-name", + organization_slug: "my-org", + }), + ), + Effect.exit, + Effect.provide( + httpClientLayer((request) => { + attempts += 1; + return attempts === 1 + ? Effect.fail(transportError(request, "connection reset")) + : Effect.succeed( + jsonResponse(request, 409, { + message: "project already exists", + }), + ); + }), + ), + ); - expect(attempts).toBe(1); - expect(Exit.isFailure(exit)).toBe(true); - }); + expect(attempts).toBe(1); + expect(Exit.isFailure(exit)).toBe(true); + }), + )); - test("does not retry transport errors for PATCH requests", async () => { - let attempts = 0; + test("does not retry transport errors for PATCH requests", () => + Effect.runPromise( + Effect.gen(function* () { + let attempts = 0; - const exit = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1PatchAMigration">(operationDefinitions.v1PatchAMigration, { - ref: "abcdefghijklmnopqrst", - version: "20240101000000", - name: "renamed", - }), - ), - Effect.exit, - Effect.provide( - httpClientLayer((request) => { - attempts += 1; - return attempts === 1 - ? Effect.fail(transportError(request, "connection reset")) - : Effect.succeed( - HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })), - ); - }), - ), - ), - ); + const exit = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1PatchAMigration">(operationDefinitions.v1PatchAMigration, { + ref: "abcdefghijklmnopqrst", + version: "20240101000000", + name: "renamed", + }), + ), + Effect.exit, + Effect.provide( + httpClientLayer((request) => { + attempts += 1; + return attempts === 1 + ? Effect.fail(transportError(request, "connection reset")) + : Effect.succeed( + HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })), + ); + }), + ), + ); - expect(attempts).toBe(1); - expect(Exit.isFailure(exit)).toBe(true); - }); + expect(attempts).toBe(1); + expect(Exit.isFailure(exit)).toBe(true); + }), + )); - test("stops after the configured number of transport retries", async () => { - const { requestTimes, exit } = await onVirtualClock( + test("stops after the configured number of transport retries", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall( - listProjects, - (request) => transportError(request, "still broken"), - { retry: { maxRetries: 2 } }, + const { requestTimes, exit } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall( + listProjects, + (request) => transportError(request, "still broken"), + { retry: { maxRetries: 2 } }, + ); + yield* TestClock.adjust(Duration.minutes(1)); + return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; - }), - ); - expect(requestTimes).toHaveLength(3); - expect(Exit.isFailure(exit)).toBe(true); - }); + expect(requestTimes).toHaveLength(3); + expect(Exit.isFailure(exit)).toBe(true); + }), + )); - test("decodes text responses through the unified execute path", async () => { - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1DiffABranch">(operationDefinitions.v1DiffABranch, { - // Must satisfy the oneOf union's project-ref/uuid pattern; a free-form - // string like "branch-ref" doesn't match either branch. - branch_id_or_ref: "abcdefghijklmnopqrst", - }), - ), - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response("select * from test;", { - status: 200, - headers: { - "content-type": "text/plain", - }, - }), + test("decodes text responses through the unified execute path", () => + Effect.runPromise( + Effect.gen(function* () { + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1DiffABranch">(operationDefinitions.v1DiffABranch, { + // Must satisfy the oneOf union's project-ref/uuid pattern; a free-form + // string like "branch-ref" doesn't match either branch. + branch_id_or_ref: "abcdefghijklmnopqrst", + }), + ), + Effect.provide( + httpClientLayer((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response("select * from test;", { + status: 200, + headers: { + "content-type": "text/plain", + }, + }), + ), ), ), ), - ), - ), - ); + ); - expect(result).toBe("select * from test;"); - }); + expect(result).toBe("select * from test;"); + }), + )); - test("decodes void responses through the unified execute path", async () => { - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1DisablePreviewBranching">( - operationDefinitions.v1DisablePreviewBranching, - { - ref: "abcdefghijklmnopqrst", - }, + test("decodes void responses through the unified execute path", () => + Effect.runPromise( + Effect.gen(function* () { + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1DisablePreviewBranching">( + operationDefinitions.v1DisablePreviewBranching, + { + ref: "abcdefghijklmnopqrst", + }, + ), ), - ), - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - HttpClientResponse.fromWeb( - request, - new Response(null, { - status: 204, - }), + Effect.provide( + httpClientLayer((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(null, { + status: 204, + }), + ), ), ), ), - ), - ), - ); + ); + + expect(result).toBeUndefined(); + }), + )); + + test("serializes oauth token exchange bodies as x-www-form-urlencoded", () => + Effect.runPromise( + Effect.gen(function* () { + let seenRequest: HttpClientRequest.HttpClientRequest | undefined; + + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1ExchangeOauthToken">(operationDefinitions.v1ExchangeOauthToken, { + body: { + grant_type: "authorization_code", + client_id: "11111111-1111-4111-8111-111111111111", + client_secret: "client-secret", + code: "auth-code", + code_verifier: "code-verifier", + redirect_uri: "https://example.com/callback", + resource: "https://mcp.supabase.com", + }, + }), + ), + Effect.provide( + httpClientLayer((request) => { + seenRequest = request; + return Effect.succeed(oauthTokenResponse(request)); + }), + ), + ); - expect(result).toBeUndefined(); - }); + expect(result.access_token).toBe("access-token"); + expect(seenRequest).toBeDefined(); + expect(seenRequest?.headers["content-type"]).toBe("application/x-www-form-urlencoded"); + + const url = requestUrl(seenRequest!); + expect(url.pathname).toBe("/v1/oauth/token"); + expect(Array.from(url.searchParams.keys())).toEqual([]); + + const body = new URLSearchParams(requestBodyText(seenRequest!)); + expect(body.get("grant_type")).toBe("authorization_code"); + expect(body.get("client_id")).toBe("11111111-1111-4111-8111-111111111111"); + expect(body.get("client_secret")).toBe("client-secret"); + expect(body.get("code")).toBe("auth-code"); + expect(body.get("code_verifier")).toBe("code-verifier"); + expect(body.get("redirect_uri")).toBe("https://example.com/callback"); + expect(body.get("resource")).toBe("https://mcp.supabase.com"); + expect(body.has("refresh_token")).toBe(false); + expect(body.has("scope")).toBe(false); + }), + )); - test("serializes oauth token exchange bodies as x-www-form-urlencoded", async () => { - let seenRequest: HttpClientRequest.HttpClientRequest | undefined; - - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1ExchangeOauthToken">(operationDefinitions.v1ExchangeOauthToken, { - body: { - grant_type: "authorization_code", - client_id: "11111111-1111-4111-8111-111111111111", - client_secret: "client-secret", - code: "auth-code", - code_verifier: "code-verifier", - redirect_uri: "https://example.com/callback", - resource: "https://mcp.supabase.com", - }, - }), - ), - Effect.provide( - httpClientLayer((request) => { - seenRequest = request; - return Effect.succeed(oauthTokenResponse(request)); - }), - ), - ), - ); + test("serializes refresh-token exchange bodies without omitted oauth fields", () => + Effect.runPromise( + Effect.gen(function* () { + let seenRequest: HttpClientRequest.HttpClientRequest | undefined; + + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1ExchangeOauthToken">(operationDefinitions.v1ExchangeOauthToken, { + body: { + grant_type: "refresh_token", + refresh_token: "refresh-token", + scope: "read:projects", + }, + }), + ), + Effect.provide( + httpClientLayer((request) => { + seenRequest = request; + return Effect.succeed(oauthTokenResponse(request)); + }), + ), + ); - expect(result.access_token).toBe("access-token"); - expect(seenRequest).toBeDefined(); - expect(seenRequest?.headers["content-type"]).toBe("application/x-www-form-urlencoded"); - - const url = requestUrl(seenRequest!); - expect(url.pathname).toBe("/v1/oauth/token"); - expect(Array.from(url.searchParams.keys())).toEqual([]); - - const body = new URLSearchParams(requestBodyText(seenRequest!)); - expect(body.get("grant_type")).toBe("authorization_code"); - expect(body.get("client_id")).toBe("11111111-1111-4111-8111-111111111111"); - expect(body.get("client_secret")).toBe("client-secret"); - expect(body.get("code")).toBe("auth-code"); - expect(body.get("code_verifier")).toBe("code-verifier"); - expect(body.get("redirect_uri")).toBe("https://example.com/callback"); - expect(body.get("resource")).toBe("https://mcp.supabase.com"); - expect(body.has("refresh_token")).toBe(false); - expect(body.has("scope")).toBe(false); - }); + expect(result.refresh_token).toBe("refresh-token"); - test("serializes refresh-token exchange bodies without omitted oauth fields", async () => { - let seenRequest: HttpClientRequest.HttpClientRequest | undefined; - - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1ExchangeOauthToken">(operationDefinitions.v1ExchangeOauthToken, { - body: { - grant_type: "refresh_token", - refresh_token: "refresh-token", - scope: "read:projects", - }, - }), - ), - Effect.provide( - httpClientLayer((request) => { - seenRequest = request; - return Effect.succeed(oauthTokenResponse(request)); - }), - ), - ), - ); + const body = new URLSearchParams(requestBodyText(seenRequest!)); + expect(body.get("grant_type")).toBe("refresh_token"); + expect(body.get("refresh_token")).toBe("refresh-token"); + expect(body.get("scope")).toBe("read:projects"); + expect(body.has("code")).toBe(false); + expect(body.has("client_id")).toBe(false); + }), + )); - expect(result.refresh_token).toBe("refresh-token"); + test("serializes create function requests as eszip bodies with metadata query params", () => + Effect.runPromise( + Effect.gen(function* () { + let seenRequest: HttpClientRequest.HttpClientRequest | undefined; + const body = new TextEncoder().encode("console.log('deploy create');"); - const body = new URLSearchParams(requestBodyText(seenRequest!)); - expect(body.get("grant_type")).toBe("refresh_token"); - expect(body.get("refresh_token")).toBe("refresh-token"); - expect(body.get("scope")).toBe("read:projects"); - expect(body.has("code")).toBe(false); - expect(body.has("client_id")).toBe(false); - }); + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1CreateAFunction">(operationDefinitions.v1CreateAFunction, { + ref: "abcdefghijklmnopqrst", + slug: "demo", + name: "Demo Function", + verify_jwt: true, + entrypoint_path: "functions/demo/index.ts", + import_map_path: "functions/demo/deno.json", + ezbr_sha256: "abc123", + body, + }), + ), + Effect.provide( + httpClientLayer((request) => { + seenRequest = request; + return Effect.succeed(functionResponse(request, 201)); + }), + ), + ); - test("serializes create function requests as eszip bodies with metadata query params", async () => { - let seenRequest: HttpClientRequest.HttpClientRequest | undefined; - const body = new TextEncoder().encode("console.log('deploy create');"); - - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1CreateAFunction">(operationDefinitions.v1CreateAFunction, { - ref: "abcdefghijklmnopqrst", - slug: "demo", - name: "Demo Function", - verify_jwt: true, - entrypoint_path: "functions/demo/index.ts", - import_map_path: "functions/demo/deno.json", - ezbr_sha256: "abc123", - body, - }), - ), - Effect.provide( - httpClientLayer((request) => { - seenRequest = request; - return Effect.succeed(functionResponse(request, 201)); - }), - ), - ), - ); + expect(result.slug).toBe("demo"); + expect(seenRequest).toBeDefined(); + expect(seenRequest?.headers["content-type"]).toBe("application/vnd.denoland.eszip"); + expect(requestBodyBytes(seenRequest!)).toEqual(body); + + const url = requestUrl(seenRequest!); + expect(url.pathname).toBe("/v1/projects/abcdefghijklmnopqrst/functions"); + expect(requestUrlParam(seenRequest!, "slug")).toBe("demo"); + expect(requestUrlParam(seenRequest!, "name")).toBe("Demo Function"); + expect(requestUrlParam(seenRequest!, "verify_jwt")).toBe("true"); + expect(requestUrlParam(seenRequest!, "entrypoint_path")).toBe("functions/demo/index.ts"); + expect(requestUrlParam(seenRequest!, "import_map_path")).toBe("functions/demo/deno.json"); + expect(requestUrlParam(seenRequest!, "ezbr_sha256")).toBe("abc123"); + }), + )); - expect(result.slug).toBe("demo"); - expect(seenRequest).toBeDefined(); - expect(seenRequest?.headers["content-type"]).toBe("application/vnd.denoland.eszip"); - expect(requestBodyBytes(seenRequest!)).toEqual(body); - - const url = requestUrl(seenRequest!); - expect(url.pathname).toBe("/v1/projects/abcdefghijklmnopqrst/functions"); - expect(requestUrlParam(seenRequest!, "slug")).toBe("demo"); - expect(requestUrlParam(seenRequest!, "name")).toBe("Demo Function"); - expect(requestUrlParam(seenRequest!, "verify_jwt")).toBe("true"); - expect(requestUrlParam(seenRequest!, "entrypoint_path")).toBe("functions/demo/index.ts"); - expect(requestUrlParam(seenRequest!, "import_map_path")).toBe("functions/demo/deno.json"); - expect(requestUrlParam(seenRequest!, "ezbr_sha256")).toBe("abc123"); - }); + test("serializes update function requests as eszip bodies with metadata query params", () => + Effect.runPromise( + Effect.gen(function* () { + let seenRequest: HttpClientRequest.HttpClientRequest | undefined; + const body = new TextEncoder().encode("console.log('deploy update');").buffer; - test("serializes update function requests as eszip bodies with metadata query params", async () => { - let seenRequest: HttpClientRequest.HttpClientRequest | undefined; - const body = new TextEncoder().encode("console.log('deploy update');").buffer; - - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1UpdateAFunction">(operationDefinitions.v1UpdateAFunction, { - ref: "abcdefghijklmnopqrst", - function_slug: "demo", - slug: "demo-renamed", - verify_jwt: true, - entrypoint_path: "functions/demo/index.ts", - import_map_path: "functions/demo/deno.json", - ezbr_sha256: "def456", - body, - }), - ), - Effect.provide( - httpClientLayer((request) => { - seenRequest = request; - return Effect.succeed(functionResponse(request, 200)); - }), - ), - ), - ); + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1UpdateAFunction">(operationDefinitions.v1UpdateAFunction, { + ref: "abcdefghijklmnopqrst", + function_slug: "demo", + slug: "demo-renamed", + verify_jwt: true, + entrypoint_path: "functions/demo/index.ts", + import_map_path: "functions/demo/deno.json", + ezbr_sha256: "def456", + body, + }), + ), + Effect.provide( + httpClientLayer((request) => { + seenRequest = request; + return Effect.succeed(functionResponse(request, 200)); + }), + ), + ); - expect(result.slug).toBe("demo"); - expect(seenRequest).toBeDefined(); - expect(seenRequest?.headers["content-type"]).toBe("application/vnd.denoland.eszip"); - expect(requestBodyBytes(seenRequest!)).toEqual(new Uint8Array(body)); - - const url = requestUrl(seenRequest!); - expect(url.pathname).toBe("/v1/projects/abcdefghijklmnopqrst/functions/demo"); - expect(requestUrlParam(seenRequest!, "slug")).toBe("demo-renamed"); - expect(requestUrlParam(seenRequest!, "verify_jwt")).toBe("true"); - expect(requestUrlParam(seenRequest!, "entrypoint_path")).toBe("functions/demo/index.ts"); - expect(requestUrlParam(seenRequest!, "import_map_path")).toBe("functions/demo/deno.json"); - expect(requestUrlParam(seenRequest!, "ezbr_sha256")).toBe("def456"); - }); + expect(result.slug).toBe("demo"); + expect(seenRequest).toBeDefined(); + expect(seenRequest?.headers["content-type"]).toBe("application/vnd.denoland.eszip"); + expect(requestBodyBytes(seenRequest!)).toEqual(new Uint8Array(body)); + + const url = requestUrl(seenRequest!); + expect(url.pathname).toBe("/v1/projects/abcdefghijklmnopqrst/functions/demo"); + expect(requestUrlParam(seenRequest!, "slug")).toBe("demo-renamed"); + expect(requestUrlParam(seenRequest!, "verify_jwt")).toBe("true"); + expect(requestUrlParam(seenRequest!, "entrypoint_path")).toBe("functions/demo/index.ts"); + expect(requestUrlParam(seenRequest!, "import_map_path")).toBe("functions/demo/deno.json"); + expect(requestUrlParam(seenRequest!, "ezbr_sha256")).toBe("def456"); + }), + )); - test("serializes deploy function requests as multipart bodies with json metadata", async () => { - let seenRequest: HttpClientRequest.HttpClientRequest | undefined; - - const metadata = { - entrypoint_path: "functions/demo/index.ts", - import_map_path: "functions/demo/deno.json", - static_patterns: ["functions/demo/static/**/*.js"], - verify_jwt: true, - name: "demo", - } as const; - - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v1DeployAFunction">(operationDefinitions.v1DeployAFunction, { - ref: "abcdefghijklmnopqrst", - slug: "demo", - bundleOnly: true, - body: { - metadata, - file: [new Uint8Array([1, 2, 3]), new Blob(["deno-config"])], - }, - }), - ), - Effect.provide( - httpClientLayer((request) => { - seenRequest = request; - return Effect.succeed(deployFunctionResponse(request)); - }), - ), - ), - ); + test("serializes deploy function requests as multipart bodies with json metadata", () => + Effect.runPromise( + Effect.gen(function* () { + let seenRequest: HttpClientRequest.HttpClientRequest | undefined; + + const metadata = { + entrypoint_path: "functions/demo/index.ts", + import_map_path: "functions/demo/deno.json", + static_patterns: ["functions/demo/static/**/*.js"], + verify_jwt: true, + name: "demo", + } as const; + + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v1DeployAFunction">(operationDefinitions.v1DeployAFunction, { + ref: "abcdefghijklmnopqrst", + slug: "demo", + bundleOnly: true, + body: { + metadata, + file: [new Uint8Array([1, 2, 3]), new Blob(["deno-config"])], + }, + }), + ), + Effect.provide( + httpClientLayer((request) => { + seenRequest = request; + return Effect.succeed(deployFunctionResponse(request)); + }), + ), + ); - expect(result.slug).toBe("demo"); - expect(seenRequest).toBeDefined(); + expect(result.slug).toBe("demo"); + expect(seenRequest).toBeDefined(); - const url = requestUrl(seenRequest!); - expect(url.pathname).toBe("/v1/projects/abcdefghijklmnopqrst/functions/deploy"); - expect(requestUrlParam(seenRequest!, "slug")).toBe("demo"); - expect(requestUrlParam(seenRequest!, "bundleOnly")).toBe("true"); + const url = requestUrl(seenRequest!); + expect(url.pathname).toBe("/v1/projects/abcdefghijklmnopqrst/functions/deploy"); + expect(requestUrlParam(seenRequest!, "slug")).toBe("demo"); + expect(requestUrlParam(seenRequest!, "bundleOnly")).toBe("true"); - const formData = requestFormData(seenRequest!); - expect(JSON.parse(formDataTextValue(formData, "metadata"))).toEqual(metadata); - expect(await formDataFileTexts(formData, "file")).toEqual([ - "\u0001\u0002\u0003", - "deno-config", - ]); - }); + const formData = requestFormData(seenRequest!); + expect( + yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))( + formDataTextValue(formData, "metadata"), + ), + ).toEqual(metadata); + expect(yield* formDataFileTexts(formData, "file")).toEqual([ + "\u0001\u0002\u0003", + "deno-config", + ]); + }), + )); test("rejects string raw binary bodies at schema decode time", () => { expect(() => @@ -1243,176 +1270,175 @@ describe("makeSupabaseApiClient", () => { ).toThrow(); }); - test("surfaces a 404 on a v2 operation as a distinguishable status error and wires the request identically to v1", async () => { - let seenRequest: HttpClientRequest.HttpClientRequest | undefined; - - const client = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - seenRequest = request; - return Effect.succeed( - jsonResponse(request, 404, { message: "Organization not found" }), - ); - }), - ), - ), - ); + test("surfaces a 404 on a v2 operation as a distinguishable status error and wires the request identically to v1", () => + Effect.runPromise( + Effect.gen(function* () { + let seenRequest: HttpClientRequest.HttpClientRequest | undefined; + + const client = yield* makeSupabaseApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + seenRequest = request; + return Effect.succeed( + jsonResponse(request, 404, { message: "Organization not found" }), + ); + }), + ), + ); - const error = await Effect.runPromise( - client - .execute(operationDefinitions.v2ListOrganizationMembers, { slug: "my-org" }) - .pipe(Effect.flip), - ); + const error = yield* client + .execute(operationDefinitions.v2ListOrganizationMembers, { slug: "my-org" }) + .pipe(Effect.flip); - expect(HttpClientError.isHttpClientError(error)).toBe(true); - if (!HttpClientError.isHttpClientError(error)) { - throw new Error("expected HttpClientError"); - } - expect(error.reason._tag).toBe("StatusCodeError"); - if (error.reason._tag !== "StatusCodeError") { - throw new Error("expected StatusCodeError"); - } - expect(error.reason.response.status).toBe(404); + expect(HttpClientError.isHttpClientError(error)).toBe(true); + if (!HttpClientError.isHttpClientError(error)) { + throw new Error("expected HttpClientError"); + } + expect(error.reason._tag).toBe("StatusCodeError"); + if (error.reason._tag !== "StatusCodeError") { + throw new Error("expected StatusCodeError"); + } + expect(error.reason.response.status).toBe(404); - expect(seenRequest).toBeDefined(); - expect(seenRequest?.url).toBe("https://api.supabase.com/v2/organizations/my-org/members"); - expect(seenRequest?.headers.authorization).toBe("Bearer test-token"); - }); + expect(seenRequest).toBeDefined(); + expect(seenRequest?.url).toBe("https://api.supabase.com/v2/organizations/my-org/members"); + expect(seenRequest?.headers.authorization).toBe("Bearer test-token"); + }), + )); - test("decodes a nested v2GetProjectConfig payload through the unified execute path", async () => { - const result = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.flatMap((client) => - client.execute<"v2GetProjectConfig">(operationDefinitions.v2GetProjectConfig, { - ref: "abcdefghijklmnopqrst", - }), - ), - Effect.provide( - httpClientLayer((request) => - Effect.succeed( - jsonResponse(request, 200, { - data: { - type: "project_config", - id: "abcdefghijklmnopqrst", - attributes: { - database: { - major_version: 17, - ssl_enforced: true, - network_restrictions: { - entitlement: "disallowed", - status: "stored", - allowed_cidrs: [], - }, - postgres_settings: {}, - }, - pooler: { - pool_mode: "transaction", - ignore_startup_parameters: "", - server_idle_timeout: 0, - server_lifetime: 0, - query_wait_timeout: 0, - reserve_pool_size: 0, - default_pool_size: 0, - max_client_conn: 0, - }, - auth: {}, - api: { - db_schema: "public", - db_extra_search_path: "", - max_rows: 1000, - db_pool_acquisition_timeout: 0, - db_pool: null, - }, - realtime: { - private_only: false, - max_concurrent_users: 0, - max_events_per_second: 0, - max_bytes_per_second: 0, - max_channels_per_client: 0, - max_joins_per_second: 0, - max_presence_events_per_second: 0, - max_payload_size_in_kb: 0, - presence_enabled: true, - suspend: false, - connection_pool: 0, - postgres_changes_pool: null, - }, - storage: { - file_size_limit: 0, - features: { - image_transformation: { enabled: true }, - s3_protocol: { enabled: true }, - purge_cache: { enabled: true }, - iceberg_catalog: { - enabled: false, - max_namespaces: 0, - max_tables: 0, - max_catalogs: 0, + test("decodes a nested v2GetProjectConfig payload through the unified execute path", () => + Effect.runPromise( + Effect.gen(function* () { + const result = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap((client) => + client.execute<"v2GetProjectConfig">(operationDefinitions.v2GetProjectConfig, { + ref: "abcdefghijklmnopqrst", + }), + ), + Effect.provide( + httpClientLayer((request) => + Effect.succeed( + jsonResponse(request, 200, { + data: { + type: "project_config", + id: "abcdefghijklmnopqrst", + attributes: { + database: { + major_version: 17, + ssl_enforced: true, + network_restrictions: { + entitlement: "disallowed", + status: "stored", + allowed_cidrs: [], }, - vector_buckets: { enabled: false, max_buckets: 0, max_indexes: 0 }, + postgres_settings: {}, + }, + pooler: { + pool_mode: "transaction", + ignore_startup_parameters: "", + server_idle_timeout: 0, + server_lifetime: 0, + query_wait_timeout: 0, + reserve_pool_size: 0, + default_pool_size: 0, + max_client_conn: 0, }, - capabilities: { - list_v2: true, - iceberg_catalog: true, - object_versioning: true, + auth: {}, + api: { + db_schema: "public", + db_extra_search_path: "", + max_rows: 1000, + db_pool_acquisition_timeout: 0, + db_pool: null, + }, + realtime: { + private_only: false, + max_concurrent_users: 0, + max_events_per_second: 0, + max_bytes_per_second: 0, + max_channels_per_client: 0, + max_joins_per_second: 0, + max_presence_events_per_second: 0, + max_payload_size_in_kb: 0, + presence_enabled: true, + suspend: false, + connection_pool: 0, + postgres_changes_pool: null, + }, + storage: { + file_size_limit: 0, + features: { + image_transformation: { enabled: true }, + s3_protocol: { enabled: true }, + purge_cache: { enabled: true }, + iceberg_catalog: { + enabled: false, + max_namespaces: 0, + max_tables: 0, + max_catalogs: 0, + }, + vector_buckets: { enabled: false, max_buckets: 0, max_indexes: 0 }, + }, + capabilities: { + list_v2: true, + iceberg_catalog: true, + object_versioning: true, + }, + upstream_target: "main", + migration_version: "1", }, - upstream_target: "main", - migration_version: "1", }, }, - }, - }), + }), + ), ), ), - ), - ), - ); + ); - expect(result.data.attributes.database.network_restrictions.entitlement).toBe("disallowed"); - expect(result.data.attributes.database.major_version).toBe(17); - expect(result.data.attributes.storage.upstream_target).toBe("main"); - expect(result.data.attributes.api.db_pool).toBeNull(); - }); + expect(result.data.attributes.database.network_restrictions.entitlement).toBe("disallowed"); + expect(result.data.attributes.database.major_version).toBe(17); + expect(result.data.attributes.storage.upstream_target).toBe("main"); + expect(result.data.attributes.api.db_pool).toBeNull(); + }), + )); // `style: deepObject` — every object-valued query parameter in the spec — is // one `param[key]=value` pair per entry. Serialized as a JSON blob instead, // the server reads no page size and no cursor, so a paginated walk silently // returns the first default-sized page forever. - test("expands deepObject query parameters into one pair per entry", async () => { - let seenRequest: HttpClientRequest.HttpClientRequest | undefined; - - const client = await Effect.runPromise( - makeSupabaseApiClient(config).pipe( - Effect.provide( - httpClientLayer((request) => { - seenRequest = request; - return Effect.succeed( - jsonResponse(request, 200, { - data: [], - links: { first: null, last: null, prev: null, next: null }, - }), - ); - }), - ), - ), - ); + test("expands deepObject query parameters into one pair per entry", () => + Effect.runPromise( + Effect.gen(function* () { + let seenRequest: HttpClientRequest.HttpClientRequest | undefined; + + const client = yield* makeSupabaseApiClient(config).pipe( + Effect.provide( + httpClientLayer((request) => { + seenRequest = request; + return Effect.succeed( + jsonResponse(request, 200, { + data: [], + links: { first: null, last: null, prev: null, next: null }, + }), + ); + }), + ), + ); - await Effect.runPromise( - client.execute(operationDefinitions.v2ListNotebooks, { - ref: "abcdefghijklmnopqrst", - page: { size: 100, after: "cursor-1" }, - filter: { name: "sales" }, + yield* client.execute(operationDefinitions.v2ListNotebooks, { + ref: "abcdefghijklmnopqrst", + page: { size: 100, after: "cursor-1" }, + filter: { name: "sales" }, + }); + + expect(seenRequest).toBeDefined(); + expect(requestUrlParam(seenRequest!, "page[size]")).toBe("100"); + expect(requestUrlParam(seenRequest!, "page[after]")).toBe("cursor-1"); + expect(requestUrlParam(seenRequest!, "filter[name]")).toBe("sales"); + // The un-expanded names never reach the wire. + expect(requestUrlParam(seenRequest!, "page")).toBeUndefined(); + expect(requestUrlParam(seenRequest!, "filter")).toBeUndefined(); }), - ); - - expect(seenRequest).toBeDefined(); - expect(requestUrlParam(seenRequest!, "page[size]")).toBe("100"); - expect(requestUrlParam(seenRequest!, "page[after]")).toBe("cursor-1"); - expect(requestUrlParam(seenRequest!, "filter[name]")).toBe("sales"); - // The un-expanded names never reach the wire. - expect(requestUrlParam(seenRequest!, "page")).toBeUndefined(); - expect(requestUrlParam(seenRequest!, "filter")).toBeUndefined(); - }); + )); }); describe("retry policy", () => { @@ -1437,180 +1463,200 @@ describe("retry policy", () => { const gaps = (times: ReadonlyArray) => times.slice(1).map((time, i) => time - times[i]!); - test.each([500, 503])( - "waits out the backoff before retrying a GET after a %i", - async (status) => { - const { early, afterBackoff, exit } = await onVirtualClock( - Effect.gen(function* () { - const call = yield* startVirtualCall(listProjects, (request, attempt) => - attempt === 1 ? jsonResponse(request, status, {}) : jsonResponse(request, 200, []), - ); - // The first backoff ceiling is 250ms, so a jitter draw of 0.5 waits 125ms. - yield* TestClock.adjust(Duration.millis(124)); - const early = call.requestTimes.length; - yield* TestClock.adjust(Duration.millis(1)); - const afterBackoff = call.requestTimes.length; - return { early, afterBackoff, exit: yield* Fiber.join(call.fiber) }; - }), - ); + test.each([500, 503])("waits out the backoff before retrying a GET after a %i", (status) => + Effect.runPromise( + Effect.gen(function* () { + const { early, afterBackoff, exit } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(listProjects, (request, attempt) => + attempt === 1 ? jsonResponse(request, status, {}) : jsonResponse(request, 200, []), + ); + // The first backoff ceiling is 250ms, so a jitter draw of 0.5 waits 125ms. + yield* TestClock.adjust(Duration.millis(124)); + const early = call.requestTimes.length; + yield* TestClock.adjust(Duration.millis(1)); + const afterBackoff = call.requestTimes.length; + return { early, afterBackoff, exit: yield* Fiber.join(call.fiber) }; + }), + ); - expect(early).toBe(1); - expect(afterBackoff).toBe(2); - expect(exit).toEqual(Exit.succeed([])); - }, + expect(early).toBe(1); + expect(afterBackoff).toBe(2); + expect(exit).toEqual(Exit.succeed([])); + }), + ), ); - test("backs off exponentially up to the delay cap", async () => { - const requestTimes = await onVirtualClock( + test("backs off exponentially up to the delay cap", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall( - listProjects, - (request) => jsonResponse(request, 503, {}), - { retry: { maxRetries: 8 } }, + const requestTimes = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall( + listProjects, + (request) => jsonResponse(request, 503, {}), + { retry: { maxRetries: 8 } }, + ); + yield* TestClock.adjust(Duration.minutes(1)); + yield* Fiber.join(call.fiber); + return call.requestTimes; + }), + 1, ); - yield* TestClock.adjust(Duration.minutes(1)); - yield* Fiber.join(call.fiber); - return call.requestTimes; - }), - 1, - ); - expect(gaps(requestTimes)).toEqual([250, 500, 1000, 2000, 4000, 4000, 4000, 4000]); - }); + expect(gaps(requestTimes)).toEqual([250, 500, 1000, 2000, 4000, 4000, 4000, 4000]); + }), + )); - test("retries a POST after a 429 only once its Retry-After seconds have elapsed", async () => { - const { early, afterWait, exit } = await onVirtualClock( + test("retries a POST after a 429 only once its Retry-After seconds have elapsed", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall(createProject, (request, attempt) => - attempt === 1 - ? HttpClientResponse.fromWeb( - request, - new Response("{}", { status: 429, headers: { "retry-after": "2" } }), - ) - : createdProject(request), + const { early, afterWait, exit } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(createProject, (request, attempt) => + attempt === 1 + ? HttpClientResponse.fromWeb( + request, + new Response("{}", { status: 429, headers: { "retry-after": "2" } }), + ) + : createdProject(request), + ); + yield* TestClock.adjust(Duration.millis(1999)); + const early = call.requestTimes.length; + yield* TestClock.adjust(Duration.millis(1)); + const afterWait = call.requestTimes.length; + return { early, afterWait, exit: yield* Fiber.join(call.fiber) }; + }), ); - yield* TestClock.adjust(Duration.millis(1999)); - const early = call.requestTimes.length; - yield* TestClock.adjust(Duration.millis(1)); - const afterWait = call.requestTimes.length; - return { early, afterWait, exit: yield* Fiber.join(call.fiber) }; - }), - ); - expect(early).toBe(1); - expect(afterWait).toBe(2); - expect(Exit.isSuccess(exit)).toBe(true); - }); + expect(early).toBe(1); + expect(afterWait).toBe(2); + expect(Exit.isSuccess(exit)).toBe(true); + }), + )); - test("retries a GET immediately after a 500 with Retry-After 0", async () => { - const requestTimes = await onVirtualClock( + test("retries a GET immediately after a 500 with Retry-After 0", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall(listProjects, (request, attempt) => - attempt === 1 - ? HttpClientResponse.fromWeb( - request, - new Response("{}", { status: 500, headers: { "retry-after": "0" } }), - ) - : jsonResponse(request, 200, []), + const requestTimes = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(listProjects, (request, attempt) => + attempt === 1 + ? HttpClientResponse.fromWeb( + request, + new Response("{}", { status: 500, headers: { "retry-after": "0" } }), + ) + : jsonResponse(request, 200, []), + ); + yield* TestClock.adjust(Duration.millis(0)); + yield* Fiber.join(call.fiber); + return call.requestTimes; + }), ); - yield* TestClock.adjust(Duration.millis(0)); - yield* Fiber.join(call.fiber); - return call.requestTimes; - }), - ); - expect(gaps(requestTimes)).toEqual([0]); - }); + expect(gaps(requestTimes)).toEqual([0]); + }), + )); - test("honours an HTTP-date Retry-After on a 503", async () => { - const now = Date.parse("2026-10-13T12:00:00Z"); - const requestTimes = await onVirtualClock( + test("honours an HTTP-date Retry-After on a 503", () => + Effect.runPromise( Effect.gen(function* () { - yield* TestClock.setTime(now); - const call = yield* startVirtualCall(listProjects, (request, attempt) => - attempt === 1 - ? HttpClientResponse.fromWeb( - request, - new Response("{}", { - status: 503, - headers: { "retry-after": "Tue, 13 Oct 2026 12:00:03 GMT" }, - }), - ) - : jsonResponse(request, 200, []), + const now = Date.parse("2026-10-13T12:00:00Z"); + const requestTimes = yield* onVirtualClock( + Effect.gen(function* () { + yield* TestClock.setTime(now); + const call = yield* startVirtualCall(listProjects, (request, attempt) => + attempt === 1 + ? HttpClientResponse.fromWeb( + request, + new Response("{}", { + status: 503, + headers: { "retry-after": "Tue, 13 Oct 2026 12:00:03 GMT" }, + }), + ) + : jsonResponse(request, 200, []), + ); + yield* TestClock.adjust(Duration.minutes(1)); + yield* Fiber.join(call.fiber); + return call.requestTimes; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - yield* Fiber.join(call.fiber); - return call.requestTimes; - }), - ); - expect(gaps(requestTimes)).toEqual([3000]); - }); + expect(gaps(requestTimes)).toEqual([3000]); + }), + )); - test.each(["soon", "-5", ""])( - "falls back to the backoff when Retry-After is %j", - async (retryAfter) => { - const requestTimes = await onVirtualClock( - Effect.gen(function* () { - const call = yield* startVirtualCall(listProjects, (request, attempt) => - attempt === 1 - ? HttpClientResponse.fromWeb( - request, - new Response("{}", { status: 429, headers: { "retry-after": retryAfter } }), - ) - : jsonResponse(request, 200, []), - ); - yield* TestClock.adjust(Duration.minutes(1)); - yield* Fiber.join(call.fiber); - return call.requestTimes; - }), - ); + test.each(["soon", "-5", ""])("falls back to the backoff when Retry-After is %j", (retryAfter) => + Effect.runPromise( + Effect.gen(function* () { + const requestTimes = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(listProjects, (request, attempt) => + attempt === 1 + ? HttpClientResponse.fromWeb( + request, + new Response("{}", { status: 429, headers: { "retry-after": retryAfter } }), + ) + : jsonResponse(request, 200, []), + ); + yield* TestClock.adjust(Duration.minutes(1)); + yield* Fiber.join(call.fiber); + return call.requestTimes; + }), + ); - expect(gaps(requestTimes)).toEqual([125]); - }, + expect(gaps(requestTimes)).toEqual([125]); + }), + ), ); - test("returns a 429 whose Retry-After exceeds the cap without retrying", async () => { - const { requestTimes, exit } = await onVirtualClock( + test("returns a 429 whose Retry-After exceeds the cap without retrying", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall(listProjects, (request) => - HttpClientResponse.fromWeb( - request, - new Response("{}", { status: 429, headers: { "retry-after": "30" } }), - ), + const { requestTimes, exit } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(listProjects, (request) => + HttpClientResponse.fromWeb( + request, + new Response("{}", { status: 429, headers: { "retry-after": "30" } }), + ), + ); + yield* TestClock.adjust(Duration.minutes(1)); + return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; - }), - ); - expect(requestTimes).toHaveLength(1); - const error = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined; - if (!HttpClientError.isHttpClientError(error) || error.reason._tag !== "StatusCodeError") { - throw new Error("expected StatusCodeError"); - } - expect(error.reason.response.status).toBe(429); - }); + expect(requestTimes).toHaveLength(1); + const error = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined; + if (!HttpClientError.isHttpClientError(error) || error.reason._tag !== "StatusCodeError") { + throw new Error("expected StatusCodeError"); + } + expect(error.reason.response.status).toBe(429); + }), + )); - test("honours a Retry-After exactly at the cap", async () => { - const requestTimes = await onVirtualClock( + test("honours a Retry-After exactly at the cap", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall(listProjects, (request, attempt) => - attempt === 1 - ? HttpClientResponse.fromWeb( - request, - new Response("{}", { status: 429, headers: { "retry-after": "10" } }), - ) - : jsonResponse(request, 200, []), + const requestTimes = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(listProjects, (request, attempt) => + attempt === 1 + ? HttpClientResponse.fromWeb( + request, + new Response("{}", { status: 429, headers: { "retry-after": "10" } }), + ) + : jsonResponse(request, 200, []), + ); + yield* TestClock.adjust(Duration.minutes(1)); + yield* Fiber.join(call.fiber); + return call.requestTimes; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - yield* Fiber.join(call.fiber); - return call.requestTimes; - }), - ); - expect(gaps(requestTimes)).toEqual([10_000]); - }); + expect(gaps(requestTimes)).toEqual([10_000]); + }), + )); describe("per-request retry options", () => { const listProjectsWith = (retry: SupabaseApiRetryOptions) => (client: SupabaseApiClientShape) => @@ -1624,120 +1670,158 @@ describe("retry policy", () => { return error.reason.response.status; }; - test("maxRetries 0 makes a single attempt and returns the 503", async () => { - const { requestTimes, exit } = await onVirtualClock( + test("maxRetries 0 makes a single attempt and returns the 503", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall(listProjectsWith({ maxRetries: 0 }), (request) => - jsonResponse(request, 503, {}), + const { requestTimes, exit } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(listProjectsWith({ maxRetries: 0 }), (request) => + jsonResponse(request, 503, {}), + ); + yield* TestClock.adjust(Duration.minutes(1)); + return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; - }), - ); - expect(requestTimes).toHaveLength(1); - expect(failedStatus(exit)).toBe(503); - }); + expect(requestTimes).toHaveLength(1); + expect(failedStatus(exit)).toBe(503); + }), + )); - test("wins over the client-level retry options, field by field", async () => { - const { requestTimes, exit } = await onVirtualClock( + test("wins over the client-level retry options, field by field", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall( - listProjectsWith({ maxRetries: 1 }), - (request) => jsonResponse(request, 503, {}), - { retry: { maxRetries: 4, requestTimeoutMs: 1000 } }, + const { requestTimes, exit } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall( + listProjectsWith({ maxRetries: 1 }), + (request) => jsonResponse(request, 503, {}), + { retry: { maxRetries: 4, requestTimeoutMs: 1000 } }, + ); + yield* TestClock.adjust(Duration.minutes(1)); + return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; - }), - ); - expect(requestTimes).toHaveLength(2); - expect(failedStatus(exit)).toBe(503); - }); + expect(requestTimes).toHaveLength(2); + expect(failedStatus(exit)).toBe(503); + }), + )); - test("still applies when transformClient wraps the client", async () => { - const { requestTimes } = await onVirtualClock( + test("still applies when transformClient wraps the client", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall( - listProjectsWith({ maxRetries: 0 }), - (request) => jsonResponse(request, 503, {}), - { - transformClient: (client) => Effect.succeed(HttpClient.mapRequest(client, (r) => r)), - }, + const { requestTimes } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall( + listProjectsWith({ maxRetries: 0 }), + (request) => jsonResponse(request, 503, {}), + { + transformClient: (client) => + Effect.succeed(HttpClient.mapRequest(client, (r) => r)), + }, + ); + yield* TestClock.adjust(Duration.minutes(1)); + yield* Fiber.join(call.fiber); + return { requestTimes: call.requestTimes }; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - yield* Fiber.join(call.fiber); - return { requestTimes: call.requestTimes }; - }), - ); - expect(requestTimes).toHaveLength(1); - }); + expect(requestTimes).toHaveLength(1); + }), + )); - test("requestTimeoutMs Infinity lets an attempt outlast the default timeout", async () => { - const { attempts, exit } = await onVirtualClock( + test("requestTimeoutMs Infinity lets an attempt outlast the default timeout", () => + Effect.runPromise( Effect.gen(function* () { - let attempts = 0; - const fiber = yield* makeSupabaseApiClient(config).pipe( - Effect.flatMap(listProjectsWith({ requestTimeoutMs: Number.POSITIVE_INFINITY })), - Effect.exit, - Effect.provide( - httpClientLayer((request) => - Effect.suspend(() => { - attempts += 1; - return Effect.as( - Effect.sleep(Duration.minutes(5)), - jsonResponse(request, 200, []), - ); - }), - ), - ), - Effect.forkChild, + const { attempts, exit } = yield* onVirtualClock( + Effect.gen(function* () { + let attempts = 0; + const fiber = yield* makeSupabaseApiClient(config).pipe( + Effect.flatMap(listProjectsWith({ requestTimeoutMs: Number.POSITIVE_INFINITY })), + Effect.exit, + Effect.provide( + httpClientLayer((request) => + Effect.suspend(() => { + attempts += 1; + return Effect.as( + Effect.sleep(Duration.minutes(5)), + jsonResponse(request, 200, []), + ); + }), + ), + ), + Effect.forkChild, + ); + yield* Effect.yieldNow; + yield* TestClock.adjust(Duration.minutes(5)); + return { attempts, exit: yield* Fiber.join(fiber) }; + }), ); - yield* Effect.yieldNow; - yield* TestClock.adjust(Duration.minutes(5)); - return { attempts, exit: yield* Fiber.join(fiber) }; - }), - ); - expect(attempts).toBe(1); - expect(exit).toEqual(Exit.succeed([])); - }); + expect(attempts).toBe(1); + expect(exit).toEqual(Exit.succeed([])); + }), + )); }); - test("does not retry a POST after a 500 however long it waits", async () => { - const requestTimes = await onVirtualClock( + test("does not retry a POST after a 500 however long it waits", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall(createProject, (request) => - jsonResponse(request, 500, {}), + const requestTimes = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(createProject, (request) => + jsonResponse(request, 500, {}), + ); + yield* TestClock.adjust(Duration.minutes(1)); + yield* Fiber.join(call.fiber); + return call.requestTimes; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - yield* Fiber.join(call.fiber); - return call.requestTimes; - }), - ); - expect(requestTimes).toHaveLength(1); - }); + expect(requestTimes).toHaveLength(1); + }), + )); - test("surfaces the last 503 once a GET exhausts its retries", async () => { - const { requestTimes, exit } = await onVirtualClock( + test("surfaces the last 503 once a GET exhausts its retries", () => + Effect.runPromise( Effect.gen(function* () { - const call = yield* startVirtualCall(listProjects, (request) => - jsonResponse(request, 503, { message: "unavailable" }), + const { requestTimes, exit } = yield* onVirtualClock( + Effect.gen(function* () { + const call = yield* startVirtualCall(listProjects, (request) => + jsonResponse(request, 503, { message: "unavailable" }), + ); + yield* TestClock.adjust(Duration.minutes(1)); + return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; + }), ); - yield* TestClock.adjust(Duration.minutes(1)); - return { requestTimes: call.requestTimes, exit: yield* Fiber.join(call.fiber) }; + + expect(requestTimes).toHaveLength(6); + const error = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined; + expect(HttpClientError.isHttpClientError(error)).toBe(true); + if (!HttpClientError.isHttpClientError(error) || error.reason._tag !== "StatusCodeError") { + throw new Error("expected StatusCodeError"); + } + expect(error.reason.response.status).toBe(503); }), - ); + )); +}); - expect(requestTimes).toHaveLength(6); - const error = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined; - expect(HttpClientError.isHttpClientError(error)).toBe(true); - if (!HttpClientError.isHttpClientError(error) || error.reason._tag !== "StatusCodeError") { - throw new Error("expected StatusCodeError"); +describe("API errors", () => { + test("keep their plain Error shape", () => { + const cause = new Error("schema failure"); + const configError = new SupabaseApiConfigError("Missing access token."); + const inputError = new SupabaseApiInputError("Invalid request."); + const causedInputError = new SupabaseApiInputError("Invalid request.", { cause }); + + for (const error of [configError, inputError, causedInputError]) { + expect(error).toBeInstanceOf(Error); + expect(Object.keys(error)).toEqual(["_tag", "name"]); + expect(error.toJSON()).toEqual({ _tag: error._tag, name: error.name }); + expect(Reflect.get(error, NodeInspectSymbol)).toBeUndefined(); } - expect(error.reason.response.status).toBe(503); + expect(String(configError)).toBe("SupabaseApiConfigError: Missing access token."); + expect("cause" in inputError).toBe(false); + expect(causedInputError.cause).toBe(cause); }); }); diff --git a/packages/api/src/internal/effect-client.ts b/packages/api/src/internal/effect-client.ts index 804c91b65e..deb13b4917 100644 --- a/packages/api/src/internal/effect-client.ts +++ b/packages/api/src/internal/effect-client.ts @@ -1,7 +1,7 @@ import { Effect } from "effect"; import type * as EffectModule from "effect/Effect"; -import { type SupabaseApiClientShape, SupabaseApiClient } from "./client.ts"; +import { type SupabaseApiClientShape, type SupabaseApiError, SupabaseApiClient } from "./client.ts"; export type EffectClient = { readonly [Key in keyof Operations]: Operations[Key] extends ( @@ -27,7 +27,7 @@ export function makeEffectApiClient( ( value as ( ...args: ReadonlyArray - ) => Effect.Effect + ) => Effect.Effect )(...args).pipe(Effect.provideService(SupabaseApiClient, client)); } if (isRecord(value)) { diff --git a/packages/api/src/internal/promise-client.ts b/packages/api/src/internal/promise-client.ts index 7742884cb4..48f9854ed2 100644 --- a/packages/api/src/internal/promise-client.ts +++ b/packages/api/src/internal/promise-client.ts @@ -1,6 +1,8 @@ import type * as ManagedRuntime from "effect/ManagedRuntime"; import type * as Effect from "effect/Effect"; +import type { SupabaseApiError } from "./client.ts"; + type PromiseClientOperations = { readonly [Key in keyof Operations]: Operations[Key] extends ( ...args: infer Args @@ -27,9 +29,11 @@ export function makePromiseClient( if (typeof value === "function") { return (...args: ReadonlyArray) => runtime.runPromise( - (value as (...args: ReadonlyArray) => Effect.Effect)( - ...args, - ), + ( + value as ( + ...args: ReadonlyArray + ) => Effect.Effect + )(...args), ); } if (isRecord(value)) { diff --git a/packages/api/src/internal/promise-client.unit.test.ts b/packages/api/src/internal/promise-client.unit.test.ts index 3bded77339..fd66c1ec57 100644 --- a/packages/api/src/internal/promise-client.unit.test.ts +++ b/packages/api/src/internal/promise-client.unit.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { Effect, Layer, ManagedRuntime, Option } from "effect"; +import { Effect, Layer, ManagedRuntime, Option, Schema } from "effect"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientError from "effect/http/HttpClientError"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; @@ -51,15 +51,10 @@ function formDataTextValue(formData: FormData, key: string): string { return value; } -async function formDataFileTexts(formData: FormData, key: string): Promise> { +function formDataFileTexts(formData: FormData, key: string): Effect.Effect> { const values = formData.getAll(key); - return Promise.all( - values.map(async (value) => { - if (typeof value === "string") { - return value; - } - return value.text(); - }), + return Effect.forEach(values, (value) => + typeof value === "string" ? Effect.succeed(value) : Effect.promise(() => value.text()), ); } @@ -78,7 +73,7 @@ const config = { } as const; describe("makePromiseClient", () => { - test("preserves only the versioned facade namespace", async () => { + test("preserves only the versioned facade namespace", () => { const seenRequests: Array<{ method: string; url: string }> = []; const runtime = ManagedRuntime.make( httpClientLayer((request) => { @@ -146,50 +141,54 @@ describe("makePromiseClient", () => { }), ); - try { - const effectClient = await runtime.runPromise(makeApiClient(config)); - const client = makePromiseClient(runtime, effectClient); + return Effect.runPromise( + Effect.gen(function* () { + const effectClient = yield* Effect.promise(() => runtime.runPromise(makeApiClient(config))); + const client = makePromiseClient(runtime, effectClient); - expect("createAProject" in client).toBe(false); - expect("getProject" in client).toBe(false); - expect("listAllProjects" in client).toBe(false); - expect(typeof client.v1.createAProject).toBe("function"); - expect(typeof client.v1.getProject).toBe("function"); - expect(typeof client.v1.listAllProjects).toBe("function"); + expect("createAProject" in client).toBe(false); + expect("getProject" in client).toBe(false); + expect("listAllProjects" in client).toBe(false); + expect(typeof client.v1.createAProject).toBe("function"); + expect(typeof client.v1.getProject).toBe("function"); + expect(typeof client.v1.listAllProjects).toBe("function"); - const created = await client.v1.createAProject({ - db_pass: "hunter2", - name: "project-name", - organization_slug: "my-org", - }); - const project = await client.v1.getProject({ - ref: "abcdefghijklmnopqrst", - }); - const projects = await client.v1.listAllProjects(); + const created = yield* Effect.promise(() => + client.v1.createAProject({ + db_pass: "hunter2", + name: "project-name", + organization_slug: "my-org", + }), + ); + const project = yield* Effect.promise(() => + client.v1.getProject({ + ref: "abcdefghijklmnopqrst", + }), + ); + const projects = yield* Effect.promise(() => client.v1.listAllProjects()); - expect(created.ref).toBe("abcdefghijklmnopqrst"); - expect(project.database.host).toBe("db.supabase.internal"); - expect(projects).toHaveLength(1); - expect(seenRequests).toEqual([ - { - method: "POST", - url: "https://api.supabase.com/v1/projects", - }, - { - method: "GET", - url: "https://api.supabase.com/v1/projects/abcdefghijklmnopqrst", - }, - { - method: "GET", - url: "https://api.supabase.com/v1/projects", - }, - ]); - } finally { - await runtime.dispose(); - } + expect(created.ref).toBe("abcdefghijklmnopqrst"); + expect(project.database.host).toBe("db.supabase.internal"); + expect(projects).toHaveLength(1); + expect(seenRequests).toEqual([ + { + method: "POST", + url: "https://api.supabase.com/v1/projects", + }, + { + method: "GET", + url: "https://api.supabase.com/v1/projects/abcdefghijklmnopqrst", + }, + { + method: "GET", + url: "https://api.supabase.com/v1/projects", + }, + ]); + }).pipe(Effect.ensuring(Effect.promise(() => runtime.dispose()))), + ); }); - test("serializes generated multipart methods through the promise facade", async () => { + test("serializes generated multipart methods through the promise facade", () => { let seenRequest: HttpClientRequest.HttpClientRequest | undefined; const runtime = ManagedRuntime.make( @@ -212,59 +211,65 @@ describe("makePromiseClient", () => { }), ); - try { - const effectClient = await runtime.runPromise(makeApiClient(config)); - const client = makePromiseClient(runtime, effectClient); + return Effect.runPromise( + Effect.gen(function* () { + const effectClient = yield* Effect.promise(() => runtime.runPromise(makeApiClient(config))); + const client = makePromiseClient(runtime, effectClient); - const metadata = { - entrypoint_path: "functions/demo/index.ts", - import_map_path: "functions/demo/deno.json", - verify_jwt: true, - name: "demo", - } as const; + const metadata = { + entrypoint_path: "functions/demo/index.ts", + import_map_path: "functions/demo/deno.json", + verify_jwt: true, + name: "demo", + } as const; - const result = await client.v1.deployAFunction({ - ref: "abcdefghijklmnopqrst", - slug: "demo", - bundleOnly: true, - body: { - metadata, - file: [new Uint8Array([1, 2, 3]), new Blob(["deno.json"])], - }, - }); + const result = yield* Effect.promise(() => + client.v1.deployAFunction({ + ref: "abcdefghijklmnopqrst", + slug: "demo", + bundleOnly: true, + body: { + metadata, + file: [new Uint8Array([1, 2, 3]), new Blob(["deno.json"])], + }, + }), + ); - expect(result.slug).toBe("demo"); - expect(new URL(seenRequest!.url).pathname).toBe( - "/v1/projects/abcdefghijklmnopqrst/functions/deploy", - ); - expect(requestUrlParam(seenRequest!, "slug")).toBe("demo"); - expect(requestUrlParam(seenRequest!, "bundleOnly")).toBe("true"); + expect(result.slug).toBe("demo"); + expect(new URL(seenRequest!.url).pathname).toBe( + "/v1/projects/abcdefghijklmnopqrst/functions/deploy", + ); + expect(requestUrlParam(seenRequest!, "slug")).toBe("demo"); + expect(requestUrlParam(seenRequest!, "bundleOnly")).toBe("true"); - const formData = requestFormData(seenRequest!); - expect(JSON.parse(formDataTextValue(formData, "metadata"))).toEqual(metadata); - expect(await formDataFileTexts(formData, "file")).toEqual([ - "\u0001\u0002\u0003", - "deno.json", - ]); - } finally { - await runtime.dispose(); - } + const formData = requestFormData(seenRequest!); + expect( + yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))( + formDataTextValue(formData, "metadata"), + ), + ).toEqual(metadata); + expect(yield* formDataFileTexts(formData, "file")).toEqual([ + "\u0001\u0002\u0003", + "deno.json", + ]); + }).pipe(Effect.ensuring(Effect.promise(() => runtime.dispose()))), + ); }); - test("disposes the runtime and rejects subsequent operations", async () => { + test("disposes the runtime and rejects subsequent operations", () => { const runtime = ManagedRuntime.make( httpClientLayer((request) => Effect.succeed(jsonResponse(request, 200, []))), ); - try { - const effectClient = await runtime.runPromise(makeApiClient(config)); - const client = makePromiseClient(runtime, effectClient); + return Effect.runPromise( + Effect.gen(function* () { + const effectClient = yield* Effect.promise(() => runtime.runPromise(makeApiClient(config))); + const client = makePromiseClient(runtime, effectClient); - await client.dispose(); + yield* Effect.promise(() => client.dispose()); - await expect(client.v1.listAllProjects()).rejects.toThrow(); - } finally { - await runtime.dispose(); - } + yield* Effect.promise(() => expect(client.v1.listAllProjects()).rejects.toThrow()); + }).pipe(Effect.ensuring(Effect.promise(() => runtime.dispose()))), + ); }); }); diff --git a/packages/api/src/node-lifecycle.integration.test.ts b/packages/api/src/node-lifecycle.integration.test.ts index 825d3426f7..1451d488b3 100644 --- a/packages/api/src/node-lifecycle.integration.test.ts +++ b/packages/api/src/node-lifecycle.integration.test.ts @@ -1,112 +1,112 @@ -import { createServer, type Server } from "node:http"; import type { Socket } from "node:net"; +import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; +import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest"; import { describe, expect, test } from "vitest"; -import { Effect } from "effect"; +import { Deferred, Effect, Fiber, Predicate } from "effect"; import * as HttpClient from "effect/http/HttpClient"; +import * as HttpServer from "effect/http/HttpServer"; +import * as HttpServerRequest from "effect/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/http/HttpServerResponse"; import { createApiClient as createNodeApiClient } from "./node.ts"; -async function startApiServer() { +const startApiServer = Effect.gen(function* () { const sockets = new Set(); - const server = createServer((_request, response) => { - response.writeHead(200, { "content-type": "application/json" }); - response.end("[]"); - }); - server.on("connection", (socket) => { - sockets.add(socket); - socket.once("close", () => sockets.delete(socket)); - }); - - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", resolve); - }); + const connection = yield* Deferred.make(); + const server = yield* HttpServer.HttpServer; + yield* server.serve( + Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest; + const socket = NodeHttpServerRequest.toIncomingMessage(request).socket; + if (!sockets.has(socket)) { + sockets.add(socket); + socket.once("close", () => sockets.delete(socket)); + yield* Deferred.succeed(connection, socket); + } + return HttpServerResponse.text("[]", { contentType: "application/json" }); + }), + ); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + for (const socket of sockets) { + socket.destroy(); + } + }), + ); - const address = server.address(); - if (address === null || typeof address === "string") { - throw new Error("Expected the API test server to have a TCP address"); + const address = server.address; + if (Predicate.isTagged(address, "UnixPathAddress")) { + return yield* Effect.die("Expected the API test server to have a TCP address"); } - return { server, sockets, url: `http://127.0.0.1:${address.port}` }; -} + return { + sockets, + connected: Deferred.await(connection), + url: `http://127.0.0.1:${address.port}`, + }; +}); -async function stopApiServer(server: Server, sockets: ReadonlySet) { - for (const socket of sockets) { - socket.destroy(); - } - await new Promise((resolve, reject) => { - server.close((error) => (error === undefined ? resolve() : reject(error))); +const socketClosed = (socket: Socket) => + Effect.callback((resume) => { + if (socket.destroyed) { + resume(Effect.void); + return; + } + const onClose = () => resume(Effect.void); + socket.once("close", onClose); + return Effect.sync(() => socket.off("close", onClose)); }); -} describe("Node client lifecycle", () => { - test("disposes the Node dispatcher and rejects operations after disposal", async () => { - const { server, sockets, url } = await startApiServer(); - let client: Awaited> | undefined; - - try { - const connected = new Promise((resolve) => server.once("connection", resolve)); - client = await createNodeApiClient({ baseUrl: url, accessToken: "test-token" }); - await expect(client.v1.listAllProjects()).resolves.toEqual([]); + test("disposes the Node dispatcher and rejects operations after disposal", () => + Effect.runPromise( + Effect.gen(function* () { + const { connected, url } = yield* startApiServer; + const client = yield* Effect.acquireRelease( + Effect.promise(() => createNodeApiClient({ baseUrl: url, accessToken: "test-token" })), + (client) => Effect.promise(() => client.dispose()), + ); + yield* Effect.promise(() => expect(client.v1.listAllProjects()).resolves.toEqual([])); - const socket = await connected; - expect(socket.destroyed).toBe(false); - const closed = new Promise((resolve) => { - if (socket.destroyed) { - resolve(); - } else { - socket.once("close", () => resolve()); - } - }); - - await client.dispose(); - await closed; - expect(socket.destroyed).toBe(true); - await expect(client.v1.listAllProjects()).rejects.toThrow(); - } finally { - if (client !== undefined) { - await client.dispose(); - } - await stopApiServer(server, sockets); - } - }); + const socket = yield* connected; + expect(socket.destroyed).toBe(false); + const closed = yield* Effect.forkChild(socketClosed(socket), { startImmediately: true }); - test("cleans up when client initialization fails", async () => { - const { server, sockets, url } = await startApiServer(); + yield* Effect.promise(() => client.dispose()); + yield* Fiber.join(closed); + expect(socket.destroyed).toBe(true); + yield* Effect.promise(() => expect(client.v1.listAllProjects()).rejects.toThrow()); + }).pipe(Effect.scoped, Effect.provide(NodeHttpServer.layerTest)), + )); - try { - const connected = new Promise((resolve) => server.once("connection", resolve)); - await expect( - createNodeApiClient( - { baseUrl: url, accessToken: "test-token" }, - { - transformClient: (client) => - Effect.gen(function* () { - yield* HttpClient.get("/initialization-check").pipe( - Effect.flatMap((response) => response.text), - Effect.orDie, - Effect.provideService(HttpClient.HttpClient, client), - ); - const socket = yield* Effect.promise(() => connected); - if (sockets.size !== 1 || socket.destroyed) { - return yield* Effect.die("initialization request socket was not open"); - } - return yield* Effect.die("client initialization failed"); - }), - }, - ), - ).rejects.toThrow("client initialization failed"); + test("cleans up when client initialization fails", () => + Effect.runPromise( + Effect.gen(function* () { + const { sockets, connected, url } = yield* startApiServer; + yield* Effect.promise(() => + expect( + createNodeApiClient( + { baseUrl: url, accessToken: "test-token" }, + { + transformClient: (client) => + Effect.gen(function* () { + yield* HttpClient.get("/initialization-check").pipe( + Effect.flatMap((response) => response.text), + Effect.orDie, + Effect.provideService(HttpClient.HttpClient, client), + ); + const socket = yield* connected; + if (sockets.size !== 1 || socket.destroyed) { + return yield* Effect.die("initialization request socket was not open"); + } + return yield* Effect.die("client initialization failed"); + }), + }, + ), + ).rejects.toThrow("client initialization failed"), + ); - const socket = await connected; - const closed = new Promise((resolve) => { - if (socket.destroyed) { - resolve(); - } else { - socket.once("close", () => resolve()); - } - }); - await closed; - expect(socket.destroyed).toBe(true); - } finally { - await stopApiServer(server, sockets); - } - }); + const socket = yield* connected; + yield* socketClosed(socket); + expect(socket.destroyed).toBe(true); + }).pipe(Effect.scoped, Effect.provide(NodeHttpServer.layerTest)), + )); }); diff --git a/packages/api/src/node.ts b/packages/api/src/node.ts index 7798542c77..f5da228d20 100644 --- a/packages/api/src/node.ts +++ b/packages/api/src/node.ts @@ -24,18 +24,15 @@ const nodeHttpClientLayer = NodeHttpClient.layerUndiciNoDispatcher.pipe( ); /** Creates a Promise API client backed by a scoped Node HTTP dispatcher. */ -export async function createApiClient( +export function createApiClient( config: SupabaseApiConfig = {}, options?: SupabaseApiClientOptions, ): Promise { const runtime = ManagedRuntime.make(nodeHttpClientLayer); - try { - const effectClient = await runtime.runPromise(makeApiClient(config, options)); - return makePromiseClient(runtime, effectClient); - } catch (error) { - await runtime.dispose(); - throw error; - } + return runtime + .runPromise(makeApiClient(config, options)) + .then((effectClient) => makePromiseClient(runtime, effectClient)) + .catch((error: unknown) => runtime.dispose().then(() => Promise.reject(error))); } /** Promise API client whose runtime and HTTP dispatcher are released by `dispose`. */ From a64030ed05eb9daf729a69710d0b6a7b18afde55 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:03:18 +0000 Subject: [PATCH 19/33] refactor(api): cover the generator scripts with effect lint (CLI-2842) (#7310) ## TL;DR brings the `packages/api` generator scripts and their tests under the effect lint, which completes the package ## whats introduced? - the allow list entry widens to `packages/api/**` - `download-openapi.ts` and `generate.ts` run as effect programs on `FileSystem`, `HttpClient`, `Config` and `Console`, with the same requests, console output, generated files and exit codes - a failed run still prints the original error and exits 1, with effect frames in the stack, and an empty `SUPABASE_API_URL` now reports effect's `UrlError` - the scripts tests move to effect, spawning turbo and oxfmt through `ChildProcess`, and the `openapi.json` sync test now checks the generator's own serializer ## ref - closes: CLI-2842 - closes: CLI-2840 --- .oxlintrc.effect.json | 2 +- .../dependent-types-check-cache.unit.test.ts | 93 +++++----- packages/api/scripts/download-openapi.ts | 162 ++++++++++++------ .../api/scripts/download-openapi.unit.test.ts | 19 +- packages/api/scripts/generate.ts | 86 +++++++--- .../generated-output-sync.unit.test.ts | 160 +++++++++-------- 6 files changed, 330 insertions(+), 192 deletions(-) diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index 4f1508de43..dd639f8829 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -6,7 +6,7 @@ }, "ignorePatterns": [ "**", - "!packages/api/src/**", + "!packages/api/**", "!packages/config/**", "!packages/stack/**", "!apps/cli/src/**", diff --git a/packages/api/scripts/dependent-types-check-cache.unit.test.ts b/packages/api/scripts/dependent-types-check-cache.unit.test.ts index f5d0b03a8e..faafa8f7fb 100644 --- a/packages/api/scripts/dependent-types-check-cache.unit.test.ts +++ b/packages/api/scripts/dependent-types-check-cache.unit.test.ts @@ -1,41 +1,53 @@ -import { execFileSync } from "node:child_process"; -import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; import { fileURLToPath } from "node:url"; +import { NodeServices } from "@effect/platform-node"; +import { Effect, Schema, Stream } from "effect"; +import * as ChildProcess from "effect/process/ChildProcess"; +import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"; import { describe, expect, test } from "vitest"; // The scheduled API sync only rewrites this package, and the project-config mirror lives in // @supabase/config, so apps/cli's type-level drift guards only run on a PR touching either if turbo // folds that package's files into the dependent's cached `types:check` hash via `transit`. -const scriptDir = path.dirname(fileURLToPath(import.meta.url)); -const packageDir = path.join(scriptDir, ".."); -const repoDir = path.join(packageDir, "..", ".."); -const turboBin = path.join(repoDir, "node_modules", ".bin", "turbo"); +const repoDir = fileURLToPath(new URL("../../..", import.meta.url)); +const turboBin = fileURLToPath(new URL("../../../node_modules/.bin/turbo", import.meta.url)); type DryRunTask = { readonly taskId: string; readonly dependencies: ReadonlyArray }; +const DryRunJson = Schema.fromJsonString( + Schema.Struct({ + tasks: Schema.Array( + Schema.Struct({ taskId: Schema.String, dependencies: Schema.Array(Schema.String) }), + ), + }), +); -function dryRunTypesCheckForDependents(packageName: string): ReadonlyArray { - // Redirects stdout to a file: Bun on Linux truncates a child's piped stdout at ~219 KB, and - // the dry-run JSON lists every hashed input file. - const tempDir = mkdtempSync(path.join(tmpdir(), "turbo-dry-run-")); - try { - const outputPath = path.join(tempDir, "dry-run.json"); - const fd = openSync(outputPath, "w"); - try { - execFileSync(turboBin, ["run", "types:check", `--filter=...${packageName}`, "--dry=json"], { - cwd: repoDir, - stdio: ["ignore", fd, "pipe"], - }); - } finally { - closeSync(fd); +const dryRunTypesCheckForDependents = (packageName: string) => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner; + const handle = yield* spawner.spawn( + ChildProcess.make( + turboBin, + ["run", "types:check", `--filter=...${packageName}`, "--dry=json"], + { + cwd: repoDir, + stdin: "ignore", + }, + ), + ); + const [exitCode, stdout, stderr] = yield* Effect.all( + [ + handle.exitCode, + Stream.mkString(Stream.decodeText(handle.stdout)), + Stream.mkString(Stream.decodeText(handle.stderr)), + ], + { concurrency: "unbounded" }, + ); + if (exitCode !== 0) { + return yield* Effect.die(`turbo dry run exited with code ${exitCode}: ${stderr}`); } - return JSON.parse(readFileSync(outputPath, "utf8")).tasks; - } finally { - rmSync(tempDir, { recursive: true, force: true }); - } -} + const { tasks } = yield* Schema.decodeEffect(DryRunJson)(stdout); + return tasks; + }).pipe(Effect.scoped); function reachableTaskIds(tasks: ReadonlyArray, from: string): ReadonlySet { const byId = new Map(tasks.map((task) => [task.taskId, task])); @@ -53,19 +65,22 @@ function reachableTaskIds(tasks: ReadonlyArray, from: string): Reado describe("dependent types:check cache key", () => { test.each(["@supabase/api", "@supabase/config"])( "includes %s's files for every workspace that depends on it", - (packageName) => { - const tasks = dryRunTypesCheckForDependents(packageName); - const dependentTypesChecks = tasks - .map((task) => task.taskId) - .filter( - (taskId) => taskId.endsWith("#types:check") && !taskId.startsWith(`${packageName}#`), - ); + (packageName) => + Effect.runPromise( + Effect.gen(function* () { + const tasks = yield* dryRunTypesCheckForDependents(packageName); + const dependentTypesChecks = tasks + .map((task) => task.taskId) + .filter( + (taskId) => taskId.endsWith("#types:check") && !taskId.startsWith(`${packageName}#`), + ); - expect(dependentTypesChecks).toContain("supabase#types:check"); - for (const taskId of dependentTypesChecks) { - expect(reachableTaskIds(tasks, taskId), taskId).toContain(`${packageName}#transit`); - } - }, + expect(dependentTypesChecks).toContain("supabase#types:check"); + for (const taskId of dependentTypesChecks) { + expect(reachableTaskIds(tasks, taskId), taskId).toContain(`${packageName}#transit`); + } + }).pipe(Effect.provide(NodeServices.layer)), + ), 60_000, ); }); diff --git a/packages/api/scripts/download-openapi.ts b/packages/api/scripts/download-openapi.ts index 48edc093fd..2fdcc1a5ce 100644 --- a/packages/api/scripts/download-openapi.ts +++ b/packages/api/scripts/download-openapi.ts @@ -1,13 +1,14 @@ #!/usr/bin/env bun -import { readFile, writeFile } from "node:fs/promises"; -import path from "node:path"; import { fileURLToPath } from "node:url"; +import { NodeServices } from "@effect/platform-node"; +import { Config, ConfigProvider, Console, Effect, FileSystem, Layer, Option, Schema } from "effect"; +import { FetchHttpClient, HttpClient } from "effect/http"; + const DEFAULT_SUPABASE_API_URL = "https://api.supabase.com"; -const scriptDir = path.dirname(fileURLToPath(import.meta.url)); -const OPENAPI_SPEC_PATH = path.join(scriptDir, "../src/generated/openapi.json"); -const OPENAPI_OVERRIDES_PATH = path.join(scriptDir, "openapi-overrides.json"); -const OPENAPI_SOURCE_PATH = path.join(scriptDir, "openapi-source.json"); +const OPENAPI_SPEC_PATH = fileURLToPath(new URL("../src/generated/openapi.json", import.meta.url)); +const OPENAPI_OVERRIDES_PATH = fileURLToPath(new URL("openapi-overrides.json", import.meta.url)); +const OPENAPI_SOURCE_PATH = fileURLToPath(new URL("openapi-source.json", import.meta.url)); const OPENAPI_DOCUMENT_VERSIONS = ["v1", "v2"] as const; type OpenApiDocumentVersion = (typeof OPENAPI_DOCUMENT_VERSIONS)[number]; @@ -40,6 +41,25 @@ type JsonPatchOperation = ( readonly $comment?: unknown; }; +class OpenApiDownloadError extends Schema.TaggedError()( + "OpenApiDownloadError", + { message: Schema.String, cause: Schema.optional(Schema.Defect()) }, +) {} + +const downloadStep = (step: () => A) => + Effect.try({ + try: step, + catch: (cause) => + new OpenApiDownloadError({ + message: cause instanceof Error ? cause.message : String(cause), + cause, + }), + }); + +const utf8 = new TextDecoder("utf-8", { ignoreBOM: true }); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); +const encodePrettyJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 })); + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } @@ -267,9 +287,11 @@ export function parseOpenApiOverrides(parsed: unknown): ReadonlyArray> { - return parseOpenApiOverrides(JSON.parse(await readFile(OPENAPI_OVERRIDES_PATH, "utf8"))); -} +export const loadOpenApiOverrides = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const source = utf8.decode(yield* fs.readFile(OPENAPI_OVERRIDES_PATH)); + return yield* downloadStep(() => parseOpenApiOverrides(JSON.parse(source))); +}); function assertOpenApiSource(value: unknown): asserts value is OpenApiSource { if (!isRecord(value) || typeof value.baseUrl !== "string") { @@ -277,27 +299,30 @@ function assertOpenApiSource(value: unknown): asserts value is OpenApiSource { } } -async function loadPinnedBaseUrl(): Promise { - let raw: string; - try { - raw = await readFile(OPENAPI_SOURCE_PATH, "utf8"); - } catch (error) { +const loadPinnedBaseUrl = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const raw = yield* fs.readFile(OPENAPI_SOURCE_PATH).pipe( + Effect.map((bytes) => Option.some(utf8.decode(bytes))), // A missing pin falls through to the default base URL; only a present // but malformed pin is an error worth stopping for. - if (isRecord(error) && error.code === "ENOENT") { - return undefined; - } - throw error; - } - const parsed = JSON.parse(raw); - assertOpenApiSource(parsed); - return parsed.baseUrl; -} - -async function writeOpenApiSource(baseUrl: string): Promise { - const source: OpenApiSource = { baseUrl }; - await writeFile(OPENAPI_SOURCE_PATH, `${JSON.stringify(source, null, 2)}\n`); -} + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(Option.none())), + ); + if (Option.isNone(raw)) { + return undefined; + } + return yield* downloadStep(() => { + const parsed = JSON.parse(raw.value); + assertOpenApiSource(parsed); + return parsed.baseUrl; + }); +}); + +const writeOpenApiSource = (baseUrl: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const source: OpenApiSource = { baseUrl }; + yield* fs.writeFileString(OPENAPI_SOURCE_PATH, `${yield* encodePrettyJson(source)}\n`); + }); export function resolveOpenApiBaseUrl({ envBaseUrl, @@ -311,7 +336,7 @@ export function resolveOpenApiBaseUrl({ } export function resolveOpenApiSpecUrl( - baseUrl = process.env.SUPABASE_API_URL, + baseUrl: string | undefined, version: OpenApiDocumentVersion = "v1", ): string { const normalizedBaseUrl = resolveOpenApiBaseUrl({ envBaseUrl: baseUrl }); @@ -432,7 +457,7 @@ export function mergeOpenApiDocuments( }; } -export function assertMergedOpenApiDocument(document: OpenApiDocument): void { +export const assertMergedOpenApiDocument = Effect.fnUntraced(function* (document: OpenApiDocument) { const operationClaims = new Map>(); for (const [pathKey, pathValue] of Object.entries(document.paths)) { @@ -450,7 +475,9 @@ export function assertMergedOpenApiDocument(document: OpenApiDocument): void { if (typeof operationId !== "string" || operationId.length === 0) { // generate.ts silently skips operations without an operationId; the // documented escape hatch is adding a "remove" override for the path. - console.warn(`OpenAPI operation ${label} has no operationId; generate.ts will skip it.`); + yield* Console.warn( + `OpenAPI operation ${label} has no operationId; generate.ts will skip it.`, + ); continue; } @@ -466,9 +493,9 @@ export function assertMergedOpenApiDocument(document: OpenApiDocument): void { const prefix = versionPrefixMatch[1]!.toLowerCase(); const leadingSegment = pathKey.split("/")[1]?.toLowerCase(); if (leadingSegment !== prefix) { - throw new Error( - `OpenAPI operationId ${JSON.stringify(operationId)} for ${label} has version prefix ${JSON.stringify(prefix)} that does not match the path's leading segment ${JSON.stringify(leadingSegment ?? "")}.`, - ); + return yield* new OpenApiDownloadError({ + message: `OpenAPI operationId ${yield* encodeJson(operationId)} for ${label} has version prefix ${yield* encodeJson(prefix)} that does not match the path's leading segment ${yield* encodeJson(leadingSegment ?? "")}.`, + }); } } } @@ -476,49 +503,74 @@ export function assertMergedOpenApiDocument(document: OpenApiDocument): void { for (const [operationId, claims] of operationClaims) { if (claims.length > 1) { - throw new Error( - `Duplicate OpenAPI operationId ${JSON.stringify(operationId)} claimed by: ${claims.join(", ")}.`, - ); + return yield* new OpenApiDownloadError({ + message: `Duplicate OpenAPI operationId ${yield* encodeJson(operationId)} claimed by: ${claims.join(", ")}.`, + }); } } -} +}); -export async function downloadOpenApiSpec(): Promise { - const envBaseUrl = process.env.SUPABASE_API_URL; +export const downloadOpenApiSpec = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const client = yield* HttpClient.HttpClient; + const envBaseUrl = Option.getOrUndefined(yield* Config.option(Config.String("SUPABASE_API_URL"))); // The pin is consulted only when the environment doesn't override it, so an // explicit SUPABASE_API_URL works even when the pin is absent or malformed. - const pinnedBaseUrl = envBaseUrl === undefined ? await loadPinnedBaseUrl() : undefined; + const pinnedBaseUrl = envBaseUrl === undefined ? yield* loadPinnedBaseUrl : undefined; const baseUrl = resolveOpenApiBaseUrl({ envBaseUrl, pinnedBaseUrl }); - console.log(`Resolved OpenAPI base URL: ${baseUrl}`); + yield* Console.log(`Resolved OpenAPI base URL: ${baseUrl}`); const documents: Array<{ readonly version: OpenApiDocumentVersion; readonly document: OpenApiDocument; }> = []; for (const { version, url } of resolveOpenApiSpecUrls(baseUrl)) { - console.log(`Fetching ${version} OpenAPI document from ${url}`); - const response = await fetch(url); + yield* Console.log(`Fetching ${version} OpenAPI document from ${url}`); + const response = yield* client.get(url); // Hard-fails on a missing document instead of tolerating it: a 404 would // silently drop the whole v2 namespace, and the hourly regeneration sync // would auto-merge that deletion unnoticed. - if (!response.ok) { - throw new Error(`Failed to download OpenAPI spec from ${url}: ${response.status}`); + if (response.status < 200 || response.status > 299) { + return yield* new OpenApiDownloadError({ + message: `Failed to download OpenAPI spec from ${url}: ${response.status}`, + }); } - const document = await response.json(); - assertOpenApiDocument(document); + const body = yield* response.json; + const document = yield* downloadStep(() => { + assertOpenApiDocument(body); + return body; + }); documents.push({ version, document }); } - const mergedDocument = mergeOpenApiDocuments(documents); - applyOpenApiOverrides(mergedDocument, await loadOpenApiOverrides()); - assertMergedOpenApiDocument(mergedDocument); + const mergedDocument = yield* downloadStep(() => mergeOpenApiDocuments(documents)); + const overrides = yield* loadOpenApiOverrides; + yield* downloadStep(() => applyOpenApiOverrides(mergedDocument, overrides)); + yield* assertMergedOpenApiDocument(mergedDocument); - await writeFile(OPENAPI_SPEC_PATH, `${JSON.stringify(mergedDocument, null, 2)}\n`); - await writeOpenApiSource(baseUrl); -} + yield* fs.writeFileString(OPENAPI_SPEC_PATH, `${yield* encodePrettyJson(mergedDocument)}\n`); + yield* writeOpenApiSource(baseUrl); +}); if (import.meta.main) { - await downloadOpenApiSpec(); + await Effect.runPromise( + downloadOpenApiSpec.pipe( + /* Crash with the step's original error so the uncaught report names it, not the wrapper. */ + Effect.catchTags({ + OpenApiDownloadError: (error) => Effect.die(error.cause ?? error), + HttpClientError: (error) => Effect.die(error.cause ?? error), + PlatformError: (error) => Effect.die(error.cause ?? error), + }), + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + FetchHttpClient.layer, + Layer.succeed(HttpClient.TracerPropagationEnabled, false), + ConfigProvider.layer(ConfigProvider.fromEnv({ preserveEmptyStrings: true })), + ), + ), + ), + ); } diff --git a/packages/api/scripts/download-openapi.unit.test.ts b/packages/api/scripts/download-openapi.unit.test.ts index 6f600f29bb..b9c84e7699 100644 --- a/packages/api/scripts/download-openapi.unit.test.ts +++ b/packages/api/scripts/download-openapi.unit.test.ts @@ -1,3 +1,5 @@ +import { NodeServices } from "@effect/platform-node"; +import { Effect } from "effect"; import { describe, expect, test, vi } from "vitest"; import { @@ -299,7 +301,7 @@ describe("download-openapi", () => { }, }; - expect(() => assertMergedOpenApiDocument(document)).toThrow( + expect(() => Effect.runSync(assertMergedOpenApiDocument(document))).toThrow( 'Duplicate OpenAPI operationId "allV2ProjectsByRefWebhooks" claimed by: GET /v2/projects/{ref}/webhooks/endpoints, GET /v2/projects/{ref}/webhooks/endpoints/{id}.', ); }); @@ -307,7 +309,7 @@ describe("download-openapi", () => { test("throws when an operationId's version prefix disagrees with its path", () => { const document = { paths: { "/v2/x": { get: { operationId: "v1-x" } } } }; - expect(() => assertMergedOpenApiDocument(document)).toThrow( + expect(() => Effect.runSync(assertMergedOpenApiDocument(document))).toThrow( 'OpenAPI operationId "v1-x" for GET /v2/x has version prefix "v1" that does not match the path\'s leading segment "v2".', ); }); @@ -316,7 +318,7 @@ describe("download-openapi", () => { const document = { paths: { "/v1/a": { get: {} } } }; const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - expect(() => assertMergedOpenApiDocument(document)).not.toThrow(); + expect(() => Effect.runSync(assertMergedOpenApiDocument(document))).not.toThrow(); expect(warnSpy).toHaveBeenCalledWith( "OpenAPI operation GET /v1/a has no operationId; generate.ts will skip it.", ); @@ -365,11 +367,14 @@ describe("download-openapi", () => { const test_ = (path: string, value: unknown = []) => ({ op: "test", path, value }); const replace = (path: string, value: unknown = ["x"]) => ({ op: "replace", path, value }); - test("accepts the committed overrides file", async () => { - const overrides = await loadOpenApiOverrides(); + test("accepts the committed overrides file", () => + Effect.runPromise( + Effect.gen(function* () { + const overrides = yield* loadOpenApiOverrides; - expect(overrides.length).toBeGreaterThan(0); - }); + expect(overrides.length).toBeGreaterThan(0); + }).pipe(Effect.provide(NodeServices.layer)), + )); test("rejects a replace that is not directly preceded by a test on the same path", () => { expect(() => diff --git a/packages/api/scripts/generate.ts b/packages/api/scripts/generate.ts index db7c7b4d7c..80c093368e 100644 --- a/packages/api/scripts/generate.ts +++ b/packages/api/scripts/generate.ts @@ -1,8 +1,8 @@ #!/usr/bin/env bun -import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import path from "node:path"; import { fileURLToPath } from "node:url"; +import { NodeServices } from "@effect/platform-node"; +import { Console, Effect, FileSystem, Path, Schema } from "effect"; import * as Arr from "effect/Array"; import * as JsonSchema from "effect/JsonSchema"; import * as SchemaRepresentation from "effect/SchemaRepresentation"; @@ -137,17 +137,35 @@ const httpMethods: Record = { head: "HEAD", }; -const scriptDir = path.dirname(fileURLToPath(import.meta.url)); -const repoRoot = path.resolve(scriptDir, "../../.."); -const sourceSpecPath = path.join(repoRoot, "packages/api/src/generated/openapi.json"); -const generatedDir = path.join(repoRoot, "packages/api/src/generated"); +const sourceSpecPath = fileURLToPath(new URL("../src/generated/openapi.json", import.meta.url)); +const generatedDir = fileURLToPath(new URL("../src/generated", import.meta.url)); + +class GenerateError extends Schema.TaggedError()("GenerateError", { + message: Schema.String, + cause: Schema.Defect(), +}) {} + +const generationStep = (step: () => A) => + Effect.try({ + try: step, + catch: (cause) => + new GenerateError({ message: cause instanceof Error ? cause.message : String(cause), cause }), + }); function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } -export function loadSpec(): OpenApiDocument { - const parsed = JSON.parse(readFileSync(sourceSpecPath, "utf8")); +export const loadSpec = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const source = new TextDecoder("utf-8", { ignoreBOM: true }).decode( + yield* fs.readFile(sourceSpecPath), + ); + return yield* generationStep(() => parseSpec(source)); +}); + +function parseSpec(source: string): OpenApiDocument { + const parsed = JSON.parse(source); if (!isRecord(parsed) || !isRecord(parsed.paths)) { throw new Error(`Invalid OpenAPI document at ${sourceSpecPath}`); } @@ -1182,21 +1200,49 @@ ${rows.join("\n")} `; } -function main() { - const document = loadSpec(); - const operations = extractOperations(document); +const encodePrettyJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 })); - rmSync(generatedDir, { recursive: true, force: true }); - mkdirSync(generatedDir, { recursive: true }); +export const renderOpenApiJson = (document: OpenApiDocument) => + Effect.map(encodePrettyJson(document), (json) => `${json}\n`); - writeFileSync(path.join(generatedDir, "contracts.ts"), renderContracts(document, operations)); - writeFileSync(path.join(generatedDir, "effect-client.ts"), renderEffectClient(operations)); - writeFileSync(path.join(generatedDir, "permissions.ts"), renderPermissions(document)); - writeFileSync(path.join(generatedDir, "openapi.json"), `${JSON.stringify(document, null, 2)}\n`); +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const document = yield* loadSpec; + const operations = yield* generationStep(() => extractOperations(document)); - console.log(`Generated ${operations.length} API operations in ${generatedDir}`); -} + yield* fs.remove(generatedDir, { recursive: true, force: true }); + yield* fs.makeDirectory(generatedDir, { recursive: true }); + + yield* fs.writeFileString( + path.join(generatedDir, "contracts.ts"), + yield* generationStep(() => renderContracts(document, operations)), + ); + yield* fs.writeFileString( + path.join(generatedDir, "effect-client.ts"), + yield* generationStep(() => renderEffectClient(operations)), + ); + yield* fs.writeFileString( + path.join(generatedDir, "permissions.ts"), + yield* generationStep(() => renderPermissions(document)), + ); + yield* fs.writeFileString( + path.join(generatedDir, "openapi.json"), + yield* renderOpenApiJson(document), + ); + + yield* Console.log(`Generated ${operations.length} API operations in ${generatedDir}`); +}); if (import.meta.main) { - main(); + await Effect.runPromise( + main.pipe( + /* Crash with the step's original error so the uncaught report names it, not the wrapper. */ + Effect.catchTags({ + GenerateError: (error) => Effect.die(error.cause), + PlatformError: (error) => Effect.die(error.cause ?? error), + }), + Effect.provide(NodeServices.layer), + ), + ); } diff --git a/packages/api/scripts/generated-output-sync.unit.test.ts b/packages/api/scripts/generated-output-sync.unit.test.ts index c9f5f3e802..ce6503e324 100644 --- a/packages/api/scripts/generated-output-sync.unit.test.ts +++ b/packages/api/scripts/generated-output-sync.unit.test.ts @@ -1,7 +1,8 @@ -import { execFileSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import path from "node:path"; import { fileURLToPath } from "node:url"; +import { NodeServices } from "@effect/platform-node"; +import { Effect, FileSystem, Path } from "effect"; +import * as ChildProcess from "effect/process/ChildProcess"; +import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"; import { describe, expect, test } from "vitest"; import { @@ -9,6 +10,7 @@ import { loadSpec, renderContracts, renderEffectClient, + renderOpenApiJson, renderPermissions, } from "./generate.ts"; @@ -18,104 +20,122 @@ import { // bodies, response types, and the executor switch, which the operation-level // bijection test in src/generated-contract-sync doesn't. -const scriptDir = path.dirname(fileURLToPath(import.meta.url)); -const packageDir = path.join(scriptDir, ".."); -const repoDir = path.join(packageDir, "..", ".."); -const generatedDir = path.join(packageDir, "src", "generated"); +const packageDir = fileURLToPath(new URL("..", import.meta.url)); +const repoDir = fileURLToPath(new URL("../../..", import.meta.url)); +const generatedDir = fileURLToPath(new URL("../src/generated", import.meta.url)); // oxfmt is a repo-root devDependency configured by the root .oxfmtrc.json, // which is also how the pipeline's root fmt:fix target runs it. -const oxfmtBin = path.join(repoDir, "node_modules", ".bin", "oxfmt"); +const oxfmtBin = fileURLToPath(new URL("../../../node_modules/.bin/oxfmt", import.meta.url)); +const utf8 = new TextDecoder("utf-8", { ignoreBOM: true }); -function formatWithOxfmt(source: string, fileName: string): string { - // Runs oxfmt in file mode, not via stdin/stdout: Bun on Linux truncates a - // child's piped stdout at ~219 KB, and these renders are 600+ KB. The temp - // dir lives inside the package (same oxfmt config) but outside node_modules, - // which oxfmt skips by default. - const tempDir = mkdtempSync(path.join(packageDir, ".generated-output-sync-")); - try { +const formatWithOxfmt = (source: string, fileName: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const spawner = yield* ChildProcessSpawner; + // Runs oxfmt in file mode, not via stdin/stdout: Bun on Linux truncates a + // child's piped stdout at ~219 KB, and these renders are 600+ KB. The temp + // dir lives inside the package (same oxfmt config) but outside node_modules, + // which oxfmt skips by default. + const tempDir = yield* fs.makeTempDirectoryScoped({ + directory: packageDir, + prefix: ".generated-output-sync-", + }); const tempFile = path.join(tempDir, fileName); - writeFileSync(tempFile, source); - execFileSync(oxfmtBin, [tempFile], { cwd: repoDir }); - return readFileSync(tempFile, "utf8"); - } finally { - rmSync(tempDir, { recursive: true, force: true }); - } -} + yield* fs.writeFileString(tempFile, source); + const exitCode = yield* spawner.exitCode( + ChildProcess.make(oxfmtBin, [tempFile], { + cwd: repoDir, + stdin: "ignore", + stdout: "ignore", + stderr: "inherit", + }), + ); + if (exitCode !== 0) { + return yield* Effect.die(`oxfmt exited with code ${exitCode} for ${tempFile}`); + } + return utf8.decode(yield* fs.readFile(tempFile)); + }).pipe(Effect.scoped); -function committedFile(fileName: string): string { - return readFileSync(path.join(generatedDir, fileName), "utf8"); -} +const committedFile = (fileName: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + return utf8.decode(yield* fs.readFile(path.join(generatedDir, fileName))); + }); -function expectSameSource(rendered: string, fileName: string): void { - const committed = committedFile(fileName); - if (rendered === committed) { - return; - } - const renderedLines = rendered.split("\n"); - const committedLines = committed.split("\n"); - const limit = Math.min(renderedLines.length, committedLines.length); - let line = 0; - while (line < limit && renderedLines[line] === committedLines[line]) { - line += 1; - } - expect.fail( - `src/generated/${fileName} is not what the generator renders from the committed snapshot ` + - `(first difference at line ${line + 1}):\n` + - ` committed: ${JSON.stringify(committedLines[line] ?? "")}\n` + - ` rendered: ${JSON.stringify(renderedLines[line] ?? "")}\n` + - `Hand edits to src/generated are not allowed — run \`pnpm generate\` instead.`, - ); -} +const expectSameSource = (rendered: string, fileName: string) => + Effect.map(committedFile(fileName), (committed) => { + if (rendered === committed) { + return; + } + const renderedLines = rendered.split("\n"); + const committedLines = committed.split("\n"); + const limit = Math.min(renderedLines.length, committedLines.length); + let line = 0; + while (line < limit && renderedLines[line] === committedLines[line]) { + line += 1; + } + expect.fail( + `src/generated/${fileName} is not what the generator renders from the committed snapshot ` + + `(first difference at line ${line + 1}):\n` + + ` committed: ${JSON.stringify(committedLines[line] ?? "")}\n` + + ` rendered: ${JSON.stringify(renderedLines[line] ?? "")}\n` + + `Hand edits to src/generated are not allowed — run \`pnpm generate\` instead.`, + ); + }); // Rendering contracts.ts runs the real schema codegen for every operation, // which takes well over vitest's default 5s budget. const RENDER_TIMEOUT_MS = 120_000; +const document = await Effect.runPromise(loadSpec.pipe(Effect.provide(NodeServices.layer))); + describe("generated output sync", () => { - const document = loadSpec(); const operations = extractOperations(document); test( "contracts.ts is byte-identical to the generator's render of the committed snapshot", { timeout: RENDER_TIMEOUT_MS }, - () => { - expectSameSource( - formatWithOxfmt(renderContracts(document, operations), "contracts.ts"), - "contracts.ts", - ); - }, + () => + formatWithOxfmt(renderContracts(document, operations), "contracts.ts").pipe( + Effect.flatMap((formatted) => expectSameSource(formatted, "contracts.ts")), + Effect.provide(NodeServices.layer), + Effect.runPromise, + ), ); test( "effect-client.ts is byte-identical to the generator's render of the committed snapshot", { timeout: RENDER_TIMEOUT_MS }, - () => { - expectSameSource( - formatWithOxfmt(renderEffectClient(operations), "effect-client.ts"), - "effect-client.ts", - ); - }, + () => + formatWithOxfmt(renderEffectClient(operations), "effect-client.ts").pipe( + Effect.flatMap((formatted) => expectSameSource(formatted, "effect-client.ts")), + Effect.provide(NodeServices.layer), + Effect.runPromise, + ), ); test( "permissions.ts is byte-identical to the generator's render of the committed snapshot", { timeout: RENDER_TIMEOUT_MS }, - () => { - expectSameSource( - formatWithOxfmt(renderPermissions(document), "permissions.ts"), - "permissions.ts", - ); - }, + () => + formatWithOxfmt(renderPermissions(document), "permissions.ts").pipe( + Effect.flatMap((formatted) => expectSameSource(formatted, "permissions.ts")), + Effect.provide(NodeServices.layer), + Effect.runPromise, + ), ); test( "openapi.json is byte-identical to the generator's normalized rewrite of itself", { timeout: RENDER_TIMEOUT_MS }, - () => { - expectSameSource( - formatWithOxfmt(`${JSON.stringify(document, null, 2)}\n`, "openapi.json"), - "openapi.json", - ); - }, + () => + renderOpenApiJson(document).pipe( + Effect.flatMap((json) => formatWithOxfmt(json, "openapi.json")), + Effect.flatMap((formatted) => expectSameSource(formatted, "openapi.json")), + Effect.provide(NodeServices.layer), + Effect.runPromise, + ), ); }); From 0238c48a94a5129970464009cd6e75d16ec06479 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:01:57 +0000 Subject: [PATCH 20/33] test(cli): cover `apps/cli/tests` with effect lint (CLI-2852) (#7327) ## TL;DR brings `apps/cli/tests` and `apps/cli/vitest.config.ts` under the effect lint ## whats introduced? - `apps/cli/tests/**` and `apps/cli/vitest.config.ts` join the allow list, replacing the seven per-file `tests/helpers` entries - the test helpers, smoke tests and vitest config run on `FileSystem`, `Path`, `Config` and `ChildProcess`, and promise helpers like `runSupabase` keep their call shape and reject with the same values - `spawnSupabase` and `makeTempHome` become scoped effects, and `seedLegacyStateDecoys` and `setLinkedRefFile` become effects, so their e2e and integration call sites yield them - a timed out `until` run now kills its child and removes its temp home instead of leaving both behind ## ref - closes: CLI-2852 --- .oxlintrc.effect.json | 11 +- .../db-bootstrap/shadow-cache.e2e.test.ts | 8 +- ...e-runtime-script.layer.integration.test.ts | 2 +- .../bootstrap/bootstrap.integration.test.ts | 2 +- .../bootstrap/bootstrap.layers.unit.test.ts | 2 +- ...ootstrap.workdir-cache.integration.test.ts | 2 +- .../commands/db/dump/dump.integration.test.ts | 16 +- .../commands/db/pull/pull.integration.test.ts | 32 +- .../commands/db/push/push.integration.test.ts | 24 +- .../db/reset/reset.integration.test.ts | 2 +- .../declarative/sync/sync.integration.test.ts | 34 +- .../stack/start/start.e2e.test.ts | 108 +- .../stack/start/start.integration.test.ts | 2 +- .../stack/start/start.storage.e2e.test.ts | 5 +- .../deploy/deploy.integration.test.ts | 4 +- .../functions/new/new.stack.e2e.test.ts | 3 +- .../functions/serve/serve.stack.e2e.test.ts | 40 +- .../serve/serve.stack.integration.test.ts | 2 +- .../gen/types/types.integration.test.ts | 2 +- .../db/inspect-query.integration.test.ts | 24 +- .../inspect/report/report.integration.test.ts | 24 +- .../link/link.stack-mode.integration.test.ts | 20 +- .../commands/login/login.integration.test.ts | 2 +- .../migration/fetch/fetch.integration.test.ts | 22 +- .../migration/list/list.integration.test.ts | 22 +- .../repair/repair.integration.test.ts | 24 +- .../seed/buckets/buckets.integration.test.ts | 2 +- .../status/status.integration.test.ts | 20 +- ...target-default-warning.integration.test.ts | 28 +- .../unlink/unlink.integration.test.ts | 18 +- .../src/commands/whoami/whoami.e2e.test.ts | 8 +- apps/cli/tests/e2e-sequencer.ts | 87 +- apps/cli/tests/e2e-sequencer.unit.test.ts | 128 +- apps/cli/tests/e2e-setup.ts | 4 +- .../tests/fixtures/compiled-cli-version.ts | 3 +- .../tests/fixtures/compiled-libpg-query.ts | 3 +- .../helpers/branches-live.integration.test.ts | 65 +- apps/cli/tests/helpers/cli.ts | 1186 ++++++++++------- apps/cli/tests/helpers/command-mocks.ts | 113 +- apps/cli/tests/helpers/docker-image.ts | 61 +- .../tests/helpers/docker-image.unit.test.ts | 111 +- apps/cli/tests/helpers/legacy-state-guard.ts | 35 +- apps/cli/tests/helpers/live-env.ts | 90 +- apps/cli/tests/helpers/live-env.unit.test.ts | 43 +- .../helpers/live-project.integration.test.ts | 119 +- apps/cli/tests/helpers/live-project.ts | 373 +++--- .../tests/helpers/live-project.unit.test.ts | 89 +- apps/cli/tests/helpers/live.ts | 382 ++++-- apps/cli/tests/helpers/local-reset.ts | 14 +- apps/cli/tests/helpers/macos-signature.ts | 81 +- apps/cli/tests/helpers/mocks.ts | 99 +- apps/cli/tests/helpers/npm-registry.ts | 582 +++++--- apps/cli/tests/helpers/release-fixture.ts | 43 +- apps/cli/tests/helpers/release-shell.ts | 73 +- .../helpers/stack-cleanup.integration.test.ts | 2 +- apps/cli/tests/helpers/stack-cleanup.ts | 2 +- apps/cli/tests/helpers/stack-e2e-cleanup.ts | 945 ++++++++----- apps/cli/tests/helpers/storage.ts | 38 +- .../tests/helpers/target-default-warning.ts | 27 +- .../no-raw-management-api-url.unit.test.ts | 102 +- apps/cli/tests/live-global-setup.ts | 76 +- apps/cli/tests/smoke-test-linux.ts | 335 ++--- apps/cli/tests/smoke-test-macos.ts | 335 +++-- apps/cli/tests/smoke-test-windows.ts | 284 ++-- apps/cli/tests/smoke-test.ts | 75 +- apps/cli/vitest.config.ts | 28 +- 66 files changed, 3976 insertions(+), 2572 deletions(-) diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index dd639f8829..d7582143a6 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -10,6 +10,8 @@ "!packages/config/**", "!packages/stack/**", "!apps/cli/src/**", + "!apps/cli/tests/**", + "!apps/cli/vitest.config.ts", // Last match wins across the whole list: keep bare re-exclusions after every // `!` entry that would otherwise re-include them. "apps/cli/src/shared/cli/bin.ts", @@ -17,13 +19,6 @@ "apps/cli/src/shared/feedback/database.types.ts", "apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts", "apps/cli/src/command-internal/db-bootstrap/local-db-running.ts", - "packages/api/src/generated/contracts.ts", - "!apps/cli/tests/helpers/branches-live.ts", - "!apps/cli/tests/helpers/compute.ts", - "!apps/cli/tests/helpers/migration-live.ts", - "!apps/cli/tests/helpers/notebooks.ts", - "!apps/cli/tests/helpers/postgres-config-live.ts", - "!apps/cli/tests/helpers/secrets-live.ts", - "!apps/cli/tests/helpers/storage-live.ts" + "packages/api/src/generated/contracts.ts" ] } diff --git a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.e2e.test.ts b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.e2e.test.ts index 135cd87894..bbf6a06113 100644 --- a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.e2e.test.ts +++ b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.e2e.test.ts @@ -19,7 +19,7 @@ import { BunServices } from "@effect/platform-bun"; import { Effect, FileSystem, Option, Path } from "effect"; import { afterEach, describe, expect, test } from "vitest"; -import { makeTempHome, runSupabase } from "../../../tests/helpers/cli.ts"; +import { createTempHome, runSupabase, type TempHome } from "../../../tests/helpers/cli.ts"; const CLEANUP_TIMEOUT_MS = 120_000; @@ -97,7 +97,7 @@ const mtimeMs = Effect.fnUntraced(function* (filePath: string) { describe("shadow baseline cache (e2e, local Docker stack)", () => { let projectDir: string | undefined; - let home: ReturnType | undefined; + let home: TempHome | undefined; afterEach(() => Effect.runPromise( @@ -116,7 +116,7 @@ describe("shadow baseline cache (e2e, local Docker stack)", () => { yield* fs.remove(projectDir, { recursive: true, force: true }).pipe(Effect.ignore); } // Disposes the temp `SUPABASE_HOME`, and with it the ~90MB baseline tar this suite published. - home?.[Symbol.dispose](); + if (home !== undefined) yield* home.dispose; projectDir = undefined; home = undefined; }).pipe(Effect.provide(BunServices.layer)), @@ -140,7 +140,7 @@ describe("shadow baseline cache (e2e, local Docker stack)", () => { projectDir = yield* fs.makeTempDirectory({ prefix: "sb-shadow-cache-e2e-" }); // One temp `SUPABASE_HOME` per test run so both `db diff` processes share the same cache // directory; `runSupabase` otherwise mints a fresh home per invocation. - home = makeTempHome(); + home = yield* createTempHome; const homeDir = home.dir; const cacheDir = path.join(home.dir, "cache", "shadow-baseline"); diff --git a/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts b/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts index 572df0bea5..1ac039779d 100644 --- a/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts +++ b/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts @@ -214,7 +214,7 @@ describe("edgeRuntimeScriptLayer sentinel handling", () => { ), ), ); - seedLegacyStateDecoys(workdir); + yield* seedLegacyStateDecoys(workdir); const { layer, docker } = setup( { exitCode: 1, stdout: "", stderr: "main worker has been destroyed\n" }, diff --git a/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts b/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts index 3694703aa9..53e079c419 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts @@ -249,7 +249,7 @@ function setup(path: Path.Path, opts: SetupOpts = {}) { dbConnectionLayer, loginApi.layer, loginCrypto.layer, - mockBrowser(), + mockBrowser, mockStdin(opts.stdinIsTty ?? true), Layer.succeed(OutputFlag, Option.none()), Layer.succeed(WorkdirFlag, opts.workdir ?? Option.some(tempRoot.current)), diff --git a/apps/cli/src/commands/bootstrap/bootstrap.layers.unit.test.ts b/apps/cli/src/commands/bootstrap/bootstrap.layers.unit.test.ts index 5a5518c27c..c9dd4fbe27 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.layers.unit.test.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.layers.unit.test.ts @@ -92,7 +92,7 @@ function ambientStubs() { isolatedHomeLayer(tempRoot.current, { SUPABASE_ACCESS_TOKEN: VALID_TOKEN }), mockTty(), mockProcessControl().layer, - mockBrowser(), + mockBrowser, mockStdin(false), analytics.layer, mockTelemetryRuntime(), diff --git a/apps/cli/src/commands/bootstrap/bootstrap.workdir-cache.integration.test.ts b/apps/cli/src/commands/bootstrap/bootstrap.workdir-cache.integration.test.ts index 04b3cb7bb0..f6d2154140 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.workdir-cache.integration.test.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.workdir-cache.integration.test.ts @@ -236,7 +236,7 @@ describe("bootstrap linked-project cache location", () => { dbConnectionLayer, mockLoginApi({ gotrueId: "gotrue-user" }).layer, mockLoginCrypto().layer, - mockBrowser(), + mockBrowser, mockStdin(true), flagsLayer, debugLogger, diff --git a/apps/cli/src/commands/db/dump/dump.integration.test.ts b/apps/cli/src/commands/db/dump/dump.integration.test.ts index fe1c2f9435..2c6a66b79c 100644 --- a/apps/cli/src/commands/db/dump/dump.integration.test.ts +++ b/apps/cli/src/commands/db/dump/dump.integration.test.ts @@ -1774,14 +1774,14 @@ describe("db dump target default warning", () => { { label: "--project-ref", over: { projectRef: Option.some("flagflagflagflagflag") } }, ] as const; - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tmp.current, linked); - const { layer, out } = setup({ conn: REMOTE_CONN, isLocal: false, workdir: tmp.current }); - return dbDump(flags({ dryRun: true, ...selector?.over })).pipe( - Effect.provide(layer), - Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), - ); - }; + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tmp.current, linked), () => { + const { layer, out } = setup({ conn: REMOTE_CONN, isLocal: false, workdir: tmp.current }); + return dbDump(flags({ dryRun: true, ...selector?.over })).pipe( + Effect.provide(layer), + Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), + ); + }); it.effect( "warns on a bare run in a linked workdir and stays silent with every selector or when unlinked", diff --git a/apps/cli/src/commands/db/pull/pull.integration.test.ts b/apps/cli/src/commands/db/pull/pull.integration.test.ts index 249c6eb24f..3b900a166b 100644 --- a/apps/cli/src/commands/db/pull/pull.integration.test.ts +++ b/apps/cli/src/commands/db/pull/pull.integration.test.ts @@ -2540,23 +2540,23 @@ describe("db pull target default warning", () => { { label: "--project-ref", over: { projectRef: Option.some("flagflagflagflagflag") } }, ] as const; - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tmp.current, linked); - const s = setup(tmp.current, { - migrations: ["20240101000000"], - remoteVersions: ["20240101000000"], - edgeStdout: "", - projectId: Option.none(), + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tmp.current, linked), () => { + const s = setup(tmp.current, { + migrations: ["20240101000000"], + remoteVersions: ["20240101000000"], + edgeStdout: "", + projectId: Option.none(), + }); + return dbPull(flags(selector?.over)).pipe( + Effect.exit, + Effect.provide(s.layer), + Effect.map(() => ({ + stdout: streamText(s.out, "stdout"), + stderr: streamText(s.out, "stderr"), + })), + ); }); - return dbPull(flags(selector?.over)).pipe( - Effect.exit, - Effect.provide(s.layer), - Effect.map(() => ({ - stdout: streamText(s.out, "stdout"), - stderr: streamText(s.out, "stderr"), - })), - ); - }; it.effect( "warns on a bare run in a linked workdir and stays silent with every selector or when unlinked", diff --git a/apps/cli/src/commands/db/push/push.integration.test.ts b/apps/cli/src/commands/db/push/push.integration.test.ts index 8401cbb6d6..035a77c398 100644 --- a/apps/cli/src/commands/db/push/push.integration.test.ts +++ b/apps/cli/src/commands/db/push/push.integration.test.ts @@ -1416,19 +1416,19 @@ describe("db push target default warning", () => { }, ] as const; - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tmp.current, linked); - const { layer, out } = setup(tmp.current, { - toml: 'project_id = "test"\n', - args: ["db", "push", ...(selector?.args ?? [])], - isLocal: false, - noProjectId: true, + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tmp.current, linked), () => { + const { layer, out } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "push", ...(selector?.args ?? [])], + isLocal: false, + noProjectId: true, + }); + return dbPush({ ...DEFAULT_FLAGS, ...selector?.over }).pipe( + Effect.provide(layer), + Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), + ); }); - return dbPush({ ...DEFAULT_FLAGS, ...selector?.over }).pipe( - Effect.provide(layer), - Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), - ); - }; it.effect( "warns on a bare run in a linked workdir and stays silent with every selector or when unlinked", diff --git a/apps/cli/src/commands/db/reset/reset.integration.test.ts b/apps/cli/src/commands/db/reset/reset.integration.test.ts index 112f460b41..bf82f8350d 100644 --- a/apps/cli/src/commands/db/reset/reset.integration.test.ts +++ b/apps/cli/src/commands/db/reset/reset.integration.test.ts @@ -1309,7 +1309,6 @@ describe("db reset", () => { }); it.effect("a stack reset never reads or writes supabase/.temp", () => { - seedLegacyStateDecoys(tmp.current); const { layer, accessed } = setup(tmp.current, { toml: 'project_id = "test"\n', args: ["db", "reset", "--local"], @@ -1317,6 +1316,7 @@ describe("db reset", () => { stackBackend: true, }); return Effect.gen(function* () { + yield* seedLegacyStateDecoys(tmp.current); yield* dbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); expect(accessed.some((entry) => entry.endsWith("config.toml"))).toBe(true); expect(legacyStateAccesses(accessed)).toEqual([]); diff --git a/apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts b/apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts index 80da2461ef..582eef96a2 100644 --- a/apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts +++ b/apps/cli/src/commands/db/schema/declarative/sync/sync.integration.test.ts @@ -1087,21 +1087,25 @@ describe("db schema declarative sync integration", () => { stackBackend: true, applyFails: true, }); - seedLegacyStateDecoys(tmp.current); - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* seedDeclarative(tmp.current); - const error = yield* dbSchemaDeclarativeSync(flags({ apply: Option.some(true) })).pipe( - Effect.flip, - ); - expect(error._tag).toBe("DeclarativeApplyError"); - const pgdeltaRoot = path.join(tmp.current, ".supabase", "tmp", "pgdelta"); - const projectRoots = yield* fs.readDirectory(pgdeltaRoot); - expect(projectRoots).toHaveLength(1); - expect(yield* fs.exists(path.join(pgdeltaRoot, projectRoots[0] ?? "", "debug"))).toBe(true); - expect(legacyStateAccesses(s.accessed)).toEqual([]); - }).pipe(Effect.provide(s.layer)); + return Effect.andThen( + seedLegacyStateDecoys(tmp.current), + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* seedDeclarative(tmp.current); + const error = yield* dbSchemaDeclarativeSync(flags({ apply: Option.some(true) })).pipe( + Effect.flip, + ); + expect(error._tag).toBe("DeclarativeApplyError"); + const pgdeltaRoot = path.join(tmp.current, ".supabase", "tmp", "pgdelta"); + const projectRoots = yield* fs.readDirectory(pgdeltaRoot); + expect(projectRoots).toHaveLength(1); + expect(yield* fs.exists(path.join(pgdeltaRoot, projectRoots[0] ?? "", "debug"))).toBe( + true, + ); + expect(legacyStateAccesses(s.accessed)).toEqual([]); + }).pipe(Effect.provide(s.layer)), + ); }, ); diff --git a/apps/cli/src/commands/experimental/stack/start/start.e2e.test.ts b/apps/cli/src/commands/experimental/stack/start/start.e2e.test.ts index c58f26240c..04aabe8cdd 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.e2e.test.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.e2e.test.ts @@ -16,9 +16,11 @@ import { ChildProcessSpawner } from "effect/process"; import { parse as parseDotenv } from "dotenv"; import { afterAll, afterEach, describe, expect, test } from "vitest"; import { - makeTempHome, + createTempHome, runSupabaseEffect, + type SpawnedSupabase, spawnSupabase, + type TempHome, } from "../../../../../tests/helpers/cli.ts"; const START_TIMEOUT_MS = 15 * 60_000; @@ -162,22 +164,19 @@ const destroyStack = (home: string, stackId: string) => }), ); -const waitForOutput = ( - spawned: ReturnType, - pattern: RegExp, - timeoutMs: number, -) => - Effect.tryPromise({ - try: () => spawned.waitForOutput(pattern, timeoutMs), - catch: (cause) => - new StartE2eProcessError({ - message: "stack logs follower did not produce live output", - cause, - }), - }); +const waitForOutput = (spawned: SpawnedSupabase, pattern: RegExp, timeoutMs: number) => + spawned.waitForOutput(pattern, timeoutMs).pipe( + Effect.mapError( + (cause) => + new StartE2eProcessError({ + message: "stack logs follower did not produce live output", + cause, + }), + ), + ); describe("stack start (compiled e2e)", () => { - let home: ReturnType | undefined; + let home: TempHome | undefined; let projectDir: string | undefined; let stackId: string | undefined; let stackDestroyed = false; @@ -214,11 +213,14 @@ describe("stack start (compiled e2e)", () => { if (projectDir !== undefined) { yield* remove(projectDir, { recursive: true, force: true }); } - yield* Effect.try({ - try: () => home?.[Symbol.dispose](), - catch: (cause) => - new StartE2eProcessError({ message: "Failed to dispose temporary CLI home", cause }), - }); + if (home !== undefined) { + yield* home.dispose.pipe( + Effect.mapError( + (cause) => + new StartE2eProcessError({ message: "Failed to dispose temporary CLI home", cause }), + ), + ); + } home = undefined; projectDir = undefined; stackId = undefined; @@ -233,7 +235,7 @@ describe("stack start (compiled e2e)", () => { () => runNode( Effect.gen(function* () { - home = makeTempHome(); + home = yield* createTempHome; projectDir = yield* makeTempDirectory("/tmp/supabase-compiled-stack-start-e2e-"); yield* makeDirectory(join(projectDir, "supabase"), { recursive: true }); yield* writeText(join(projectDir, "supabase", "config.toml"), minimalConfig); @@ -339,44 +341,30 @@ describe("stack start (compiled e2e)", () => { const followResult = yield* Effect.scoped( Effect.gen(function* () { - const followed = yield* Effect.acquireRelease( - Effect.try({ - try: () => - spawnSupabase( - [ - "stack", - "logs", - "--follow", - "--stack-id", - idText, - "--service", - "database", - "--output-format", - "stream-json", - ], - { - cwd: projectRoot, - home: homeDir.dir, - env: { SUPABASE_EXPERIMENTAL_STACK: "1" }, - exitTimeoutMs: CLEANUP_TIMEOUT_MS, - }, - ), - catch: (cause) => + /* The scope owns the follower: closing it SIGKILLs the group and awaits its exit. */ + const followed = yield* spawnSupabase( + [ + "stack", + "logs", + "--follow", + "--stack-id", + idText, + "--service", + "database", + "--output-format", + "stream-json", + ], + { + cwd: projectRoot, + home: homeDir.dir, + env: { SUPABASE_EXPERIMENTAL_STACK: "1" }, + exitTimeoutMs: CLEANUP_TIMEOUT_MS, + }, + ).pipe( + Effect.mapError( + (cause) => new StartE2eProcessError({ message: "failed to spawn logs follower", cause }), - }), - (spawned) => - Effect.sync(() => spawned.releaseOwned({ successOptOut: false })).pipe( - Effect.flatMap(() => spawned.exitEffect(CLEANUP_TIMEOUT_MS)), - Effect.asVoid, - Effect.catchTag("CliHomeDisposeError", (cause) => - Effect.die( - new StartE2eProcessError({ - message: "Follower reused the shared CLI home; disposal must not fail", - cause, - }), - ), - ), - ), + ), ); // History precedes the follow, whose start is the last printed record. yield* waitForOutput( @@ -402,8 +390,8 @@ describe("stack start (compiled e2e)", () => { /"type":"log-entry".*"source":"live"/u, START_TIMEOUT_MS, ); - yield* Effect.sync(() => followed.kill("SIGINT")); - return yield* followed.exitEffect(CLEANUP_TIMEOUT_MS); + yield* followed.kill("SIGINT"); + return yield* followed.waitForExit(CLEANUP_TIMEOUT_MS); }), ); expect( diff --git a/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts b/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts index ca3c828c75..4dc30ea621 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts @@ -1003,7 +1003,7 @@ describe("experimental stack start", () => { const root = yield* fs.makeTempDirectoryScoped({ prefix: "stack-start-uploads-" }); yield* fs.makeDirectory(`${root}/supabase`, { recursive: true }); yield* fs.writeFileString(`${root}/supabase/config.toml`, 'project_id = "start-uploads"\n'); - seedLegacyStateDecoys(root); + yield* seedLegacyStateDecoys(root); const excluded = ["auth", "realtime", "functions", "mail", "analytics", "pooler"]; const fixture = fakeStack(); const recorder = recordingFileSystem(); diff --git a/apps/cli/src/commands/experimental/stack/start/start.storage.e2e.test.ts b/apps/cli/src/commands/experimental/stack/start/start.storage.e2e.test.ts index 013d848af1..316914b449 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.storage.e2e.test.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.storage.e2e.test.ts @@ -3,8 +3,8 @@ import { describe, expect, it } from "@effect/vitest"; import { Data, Effect, Exit, FileSystem, Path, Schema } from "effect"; import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import { + createTempHome, makeTempCliProject, - makeTempHome, runSupabaseEffect, } from "../../../../../tests/helpers/cli.ts"; import { stackArtifactCacheRoot } from "../../../../../tests/helpers/stack-artifacts.ts"; @@ -87,7 +87,8 @@ describe("stack start Storage behind the API gateway (compiled e2e)", () => { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const http = yield* HttpClient.HttpClient; - const home = makeTempHome(); + /* Left to the e2e cleanup drain, which disposes it with the stack project it serves. */ + const home = yield* createTempHome; const project = yield* Effect.promise(() => makeTempCliProject(`stack-storage-${runtime}-e2e-`), ); diff --git a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts index 2e47137c5f..a1bb1fe224 100644 --- a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts +++ b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts @@ -2345,7 +2345,7 @@ describe("functions deploy", () => { ), ), ); - seedLegacyStateDecoys(tempRoot.current); + yield* seedLegacyStateDecoys(tempRoot.current); yield* functionsDeploy({ ...baseFlags, useApi: false, useDocker: true }); @@ -2404,7 +2404,7 @@ describe("functions deploy", () => { ), ), ); - seedLegacyStateDecoys(tempRoot.current); + yield* seedLegacyStateDecoys(tempRoot.current); yield* functionsDeploy({ ...baseFlags, useApi: false, useDocker: true }); diff --git a/apps/cli/src/commands/functions/new/new.stack.e2e.test.ts b/apps/cli/src/commands/functions/new/new.stack.e2e.test.ts index b58f21eb08..b39e89ed84 100644 --- a/apps/cli/src/commands/functions/new/new.stack.e2e.test.ts +++ b/apps/cli/src/commands/functions/new/new.stack.e2e.test.ts @@ -76,8 +76,7 @@ describe("functions new (stack e2e)", () => { : {}), prefix: `functions-new-stack-${runtime}-`, }); - const home = makeTempHome(); - yield* Effect.addFinalizer(() => Effect.sync(() => home[Symbol.dispose]())); + const home = yield* makeTempHome; yield* fs.makeDirectory(path.join(projectDir, "supabase"), { recursive: true }); yield* fs.writeFileString( path.join(projectDir, "supabase", "config.toml"), diff --git a/apps/cli/src/commands/functions/serve/serve.stack.e2e.test.ts b/apps/cli/src/commands/functions/serve/serve.stack.e2e.test.ts index c4445956b9..44b834ca01 100644 --- a/apps/cli/src/commands/functions/serve/serve.stack.e2e.test.ts +++ b/apps/cli/src/commands/functions/serve/serve.stack.e2e.test.ts @@ -22,7 +22,7 @@ import { import { FetchHttpClient, HttpClient } from "effect/http"; import { homedir } from "node:os"; -import { spawnSupabase } from "../../../../tests/helpers/cli.ts"; +import { type SpawnedSupabase, spawnSupabase } from "../../../../tests/helpers/cli.ts"; import { generateGoJwt } from "../../../command-internal/go-jwt.ts"; import { destroyTestStack } from "../../../../tests/helpers/stack-cleanup.ts"; import { stackArtifactCacheRoot } from "../../../../tests/helpers/stack-artifacts.ts"; @@ -155,31 +155,27 @@ const serve = Effect.fn("FunctionsServeE2e.serve")(function* ( home: string, flags: ReadonlyArray = [], ) { - const child = yield* Effect.acquireRelease( - Effect.try({ - try: () => - spawnSupabase(["functions", "serve", ...flags], { - cwd: root, - home, - env: { SUPABASE_EXPERIMENTAL_STACK: "1" }, - exitTimeoutMs: 120_000, - }), - catch: (cause) => new FunctionsServeE2eError({ message: "CLI spawn failed", cause }), - }), - (process) => Effect.sync(() => process.releaseOwned({ successOptOut: false })), + const child = yield* spawnSupabase(["functions", "serve", ...flags], { + cwd: root, + home, + env: { SUPABASE_EXPERIMENTAL_STACK: "1" }, + exitTimeoutMs: 120_000, + }).pipe( + Effect.mapError((cause) => new FunctionsServeE2eError({ message: "CLI spawn failed", cause })), ); - yield* Effect.tryPromise({ - try: () => child.waitForOutput(/Serving Functions at /, 120_000), - catch: (cause) => new FunctionsServeE2eError({ message: "serve did not become ready", cause }), - }); + yield* child + .waitForOutput(/Serving Functions at /, 120_000) + .pipe( + Effect.mapError( + (cause) => new FunctionsServeE2eError({ message: "serve did not become ready", cause }), + ), + ); return child; }); -const interrupt = Effect.fn("FunctionsServeE2e.interrupt")(function* ( - child: ReturnType, -) { - yield* Effect.sync(() => child.kill("SIGINT")); - const exit = yield* child.exitEffect(120_000); +const interrupt = Effect.fn("FunctionsServeE2e.interrupt")(function* (child: SpawnedSupabase) { + yield* child.kill("SIGINT"); + const exit = yield* child.waitForExit(120_000); expect(exit.exitCode, `stdout:\n${child.stdout()}\nstderr:\n${child.stderr()}`).toBe(0); }); diff --git a/apps/cli/src/commands/functions/serve/serve.stack.integration.test.ts b/apps/cli/src/commands/functions/serve/serve.stack.integration.test.ts index 00570c44e2..fcad75174f 100644 --- a/apps/cli/src/commands/functions/serve/serve.stack.integration.test.ts +++ b/apps/cli/src/commands/functions/serve/serve.stack.integration.test.ts @@ -443,7 +443,7 @@ describe("experimental Stack Functions serve", () => { `${root}/supabase/config.toml`, 'project_id = "functions-no-legacy-state"\n\n[edge_runtime]\nenabled = true\n', ); - seedLegacyStateDecoys(root); + yield* seedLegacyStateDecoys(root); const state = yield* fixture({ standaloneProjectRoot: root }); const run = yield* functionsServeStack(flags()).pipe( Effect.provide(state.layer), diff --git a/apps/cli/src/commands/gen/types/types.integration.test.ts b/apps/cli/src/commands/gen/types/types.integration.test.ts index 78671fba60..96b4ca2a0d 100644 --- a/apps/cli/src/commands/gen/types/types.integration.test.ts +++ b/apps/cli/src/commands/gen/types/types.integration.test.ts @@ -2744,7 +2744,7 @@ describe("gen types", () => { }), ), }); - seedLegacyStateDecoys(workdir); + yield* seedLegacyStateDecoys(workdir); yield* genTypes(defaultFlags({ local: true })).pipe( Effect.provide(Layer.mergeAll(layer, stackBackendLayer("stack"))), diff --git a/apps/cli/src/commands/inspect/db/inspect-query.integration.test.ts b/apps/cli/src/commands/inspect/db/inspect-query.integration.test.ts index bfbd69ffb3..13df57e318 100644 --- a/apps/cli/src/commands/inspect/db/inspect-query.integration.test.ts +++ b/apps/cli/src/commands/inspect/db/inspect-query.integration.test.ts @@ -677,19 +677,19 @@ describe("inspect db target default warning", () => { }, ] as const; - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tempRoot.current, linked); - const { layer, out } = setup({ - rows: [DB_STATS_ROW], - cliArgs: selector?.args ?? [], - workdir: tempRoot.current, - noProjectId: true, + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tempRoot.current, linked), () => { + const { layer, out } = setup({ + rows: [DB_STATS_ROW], + cliArgs: selector?.args ?? [], + workdir: tempRoot.current, + noProjectId: true, + }); + return inspectDbDbStats(flags(selector?.over)).pipe( + Effect.provide(layer), + Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), + ); }); - return inspectDbDbStats(flags(selector?.over)).pipe( - Effect.provide(layer), - Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), - ); - }; it.effect( "warns on a bare run in a linked workdir and stays silent with every selector or when unlinked", diff --git a/apps/cli/src/commands/inspect/report/report.integration.test.ts b/apps/cli/src/commands/inspect/report/report.integration.test.ts index 0f0b34d817..b2ff393657 100644 --- a/apps/cli/src/commands/inspect/report/report.integration.test.ts +++ b/apps/cli/src/commands/inspect/report/report.integration.test.ts @@ -802,19 +802,19 @@ describe("inspect report target default warning", () => { () => Effect.gen(function* () { const base = yield* tempDir("supabase-report-out-"); - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tempRoot.current, linked); - const { layer, out } = setupReport({ - csvs: DEFAULT_RULE_CSVS, - cliArgs: selector?.args ?? [], - workdir: tempRoot.current, - noProjectId: true, + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tempRoot.current, linked), () => { + const { layer, out } = setupReport({ + csvs: DEFAULT_RULE_CSVS, + cliArgs: selector?.args ?? [], + workdir: tempRoot.current, + noProjectId: true, + }); + return inspectReport(flags({ outputDir: base, ...selector?.over })).pipe( + Effect.provide(layer), + Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), + ); }); - return inspectReport(flags({ outputDir: base, ...selector?.over })).pipe( - Effect.provide(layer), - Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), - ); - }; yield* assertTargetDefaultWarning({ command: "inspect report", sameTarget: selectors[0], diff --git a/apps/cli/src/commands/link/link.stack-mode.integration.test.ts b/apps/cli/src/commands/link/link.stack-mode.integration.test.ts index b3e9cdd585..fa54c70354 100644 --- a/apps/cli/src/commands/link/link.stack-mode.integration.test.ts +++ b/apps/cli/src/commands/link/link.stack-mode.integration.test.ts @@ -177,15 +177,17 @@ describe("link integration in stack mode", () => { it.effect("never reads or writes supabase/.temp and ignores a legacy link left there", () => { const { layer, workdir, accessed } = setup(); - seedLegacyStateDecoys(workdir); - return Effect.gen(function* () { - yield* link(flags()); - - const document = yield* readProjectLink(workdir); - expect(Option.map(document, (value) => value.project.ref)).toEqual(Option.some(VALID_REF)); - expect(accessed.some((entry) => entry.includes(".link.json"))).toBe(true); - expect(legacyStateAccesses(accessed)).toEqual([]); - }).pipe(Effect.provide(layer)); + return Effect.andThen( + seedLegacyStateDecoys(workdir), + Effect.gen(function* () { + yield* link(flags()); + + const document = yield* readProjectLink(workdir); + expect(Option.map(document, (value) => value.project.ref)).toEqual(Option.some(VALID_REF)); + expect(accessed.some((entry) => entry.includes(".link.json"))).toBe(true); + expect(legacyStateAccesses(accessed)).toEqual([]); + }).pipe(Effect.provide(layer)), + ); }); it.effect("keeps the link, its staged writes, and the stack lease out of version control", () => { diff --git a/apps/cli/src/commands/login/login.integration.test.ts b/apps/cli/src/commands/login/login.integration.test.ts index 135a60f33a..ae370fd2de 100644 --- a/apps/cli/src/commands/login/login.integration.test.ts +++ b/apps/cli/src/commands/login/login.integration.test.ts @@ -122,7 +122,7 @@ function setupLogin(opts: SetupOpts = {}) { ? loginApi.layer : loginApiLayer.pipe(Layer.provide(httpApi.httpClientLayer), Layer.provide(cliSettings)), mockStdin(isTTY, opts.pipedStdin), - mockBrowser(), + mockBrowser, Layer.succeed(ProfileFlag, opts.profileFlag ?? "supabase"), ...(opts.argv !== undefined ? [Layer.succeed(CliArgs, { args: opts.argv })] : []), ); diff --git a/apps/cli/src/commands/migration/fetch/fetch.integration.test.ts b/apps/cli/src/commands/migration/fetch/fetch.integration.test.ts index 5b8feb2bda..48b9c4e8de 100644 --- a/apps/cli/src/commands/migration/fetch/fetch.integration.test.ts +++ b/apps/cli/src/commands/migration/fetch/fetch.integration.test.ts @@ -577,18 +577,18 @@ describe("migration fetch target default warning", () => { }, ] as const; - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tmp.current, linked); - const { layer, out } = setup(tmp.current, { - cliArgs: selector?.args ?? [], - rows: [], - noProjectId: true, + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tmp.current, linked), () => { + const { layer, out } = setup(tmp.current, { + cliArgs: selector?.args ?? [], + rows: [], + noProjectId: true, + }); + return migrationFetch(flags(selector?.over)).pipe( + Effect.provide(layer), + Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), + ); }); - return migrationFetch(flags(selector?.over)).pipe( - Effect.provide(layer), - Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), - ); - }; it.effect( "warns on a bare run in a linked workdir and stays silent with every selector or when unlinked", diff --git a/apps/cli/src/commands/migration/list/list.integration.test.ts b/apps/cli/src/commands/migration/list/list.integration.test.ts index 8c5e5f3d02..1b9da24ac6 100644 --- a/apps/cli/src/commands/migration/list/list.integration.test.ts +++ b/apps/cli/src/commands/migration/list/list.integration.test.ts @@ -455,18 +455,18 @@ describe("migration list target default warning", () => { }, ] as const; - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tmp.current, linked); - const { layer, out } = setup(tmp.current, { - args: selector?.args ?? [], - remote: ["20240101000000"], - noProjectId: true, + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tmp.current, linked), () => { + const { layer, out } = setup(tmp.current, { + args: selector?.args ?? [], + remote: ["20240101000000"], + noProjectId: true, + }); + return migrationList(flags(selector?.over)).pipe( + Effect.provide(layer), + Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), + ); }); - return migrationList(flags(selector?.over)).pipe( - Effect.provide(layer), - Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), - ); - }; it.effect( "warns on a bare run in a linked workdir and stays silent with every selector or when unlinked", diff --git a/apps/cli/src/commands/migration/repair/repair.integration.test.ts b/apps/cli/src/commands/migration/repair/repair.integration.test.ts index f63bccd905..33f600d040 100644 --- a/apps/cli/src/commands/migration/repair/repair.integration.test.ts +++ b/apps/cli/src/commands/migration/repair/repair.integration.test.ts @@ -618,19 +618,19 @@ describe("migration repair target default warning", () => { }, ] as const; - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tmp.current, linked); - const { layer, out } = setup(tmp.current, { - args: selector?.args ?? [], - noProjectId: true, + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tmp.current, linked), () => { + const { layer, out } = setup(tmp.current, { + args: selector?.args ?? [], + noProjectId: true, + }); + return migrationRepair( + input({ versions: ["20240101000000"], status: "reverted", ...selector?.over }), + ).pipe( + Effect.provide(layer), + Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), + ); }); - return migrationRepair( - input({ versions: ["20240101000000"], status: "reverted", ...selector?.over }), - ).pipe( - Effect.provide(layer), - Effect.map(() => ({ stdout: out.stdoutText, stderr: out.stderrText })), - ); - }; it.effect( "warns on a bare run in a linked workdir and stays silent with every selector or when unlinked", diff --git a/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts b/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts index 6cf44c990f..856dc7443b 100644 --- a/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts +++ b/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts @@ -3075,7 +3075,7 @@ describe("stack backend", () => { { method: "POST", match: "/storage/v1/object/", body: {} }, ], }); - seedLegacyStateDecoys(tmp.current); + yield* seedLegacyStateDecoys(tmp.current); yield* seedBuckets(DEFAULT_FLAGS).pipe(Effect.provide(layer)); diff --git a/apps/cli/src/commands/status/status.integration.test.ts b/apps/cli/src/commands/status/status.integration.test.ts index 8af2abe9b5..c84f8b79e7 100644 --- a/apps/cli/src/commands/status/status.integration.test.ts +++ b/apps/cli/src/commands/status/status.integration.test.ts @@ -1314,15 +1314,17 @@ content_path = "./supabase/templates/password_changed_notification.html" "stack mode: never reads or writes supabase/.temp and ignores a legacy link there", () => { const { layer, out, workdir, accessed } = setup({ projectStateMode: "stack" }); - seedLegacyStateDecoys(workdir); - return Effect.gen(function* () { - yield* writeConfig(); - yield* status(flags()); - expect(out.stdoutText).not.toContain("Linked Project"); - expect(out.stdoutText).not.toContain("decoy"); - expect(accessed.some((entry) => entry.endsWith("config.toml"))).toBe(true); - expect(legacyStateAccesses(accessed)).toEqual([]); - }).pipe(Effect.provide(layer)); + return Effect.andThen( + seedLegacyStateDecoys(workdir), + Effect.gen(function* () { + yield* writeConfig(); + yield* status(flags()); + expect(out.stdoutText).not.toContain("Linked Project"); + expect(out.stdoutText).not.toContain("decoy"); + expect(accessed.some((entry) => entry.endsWith("config.toml"))).toBe(true); + expect(legacyStateAccesses(accessed)).toEqual([]); + }).pipe(Effect.provide(layer)), + ); }, ); diff --git a/apps/cli/src/commands/storage/storage.target-default-warning.integration.test.ts b/apps/cli/src/commands/storage/storage.target-default-warning.integration.test.ts index f74e0f2ffe..6227bbc97e 100644 --- a/apps/cli/src/commands/storage/storage.target-default-warning.integration.test.ts +++ b/apps/cli/src/commands/storage/storage.target-default-warning.integration.test.ts @@ -70,20 +70,20 @@ describe("storage target default warning", () => { ]; for (const { name, args } of leaves) { - const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => { - setLinkedRefFile(tempRoot.current, linked); - const argv = [...args, "--experimental", ...(selector?.args ?? [])]; - const { layer, out } = setup(argv); - return Command.runWith(testRoot, { version: "0.0.0-test" })(argv).pipe( - Effect.exit, - Effect.provide(layer), - Effect.map((exit) => ({ - stdout: out.stdoutText, - stderr: out.stderrText, - failure: describeFailure(exit), - })), - ); - }; + const runWith = (linked: boolean) => (selector: (typeof selectors)[number] | undefined) => + Effect.andThen(setLinkedRefFile(tempRoot.current, linked), () => { + const argv = [...args, "--experimental", ...(selector?.args ?? [])]; + const { layer, out } = setup(argv); + return Command.runWith(testRoot, { version: "0.0.0-test" })(argv).pipe( + Effect.exit, + Effect.provide(layer), + Effect.map((exit) => ({ + stdout: out.stdoutText, + stderr: out.stderrText, + failure: describeFailure(exit), + })), + ); + }); it.effect( `${name} warns on a bare run in a linked workdir and stays silent with every selector or when unlinked`, diff --git a/apps/cli/src/commands/unlink/unlink.integration.test.ts b/apps/cli/src/commands/unlink/unlink.integration.test.ts index ddf73cc1f8..a5f1180663 100644 --- a/apps/cli/src/commands/unlink/unlink.integration.test.ts +++ b/apps/cli/src/commands/unlink/unlink.integration.test.ts @@ -388,14 +388,16 @@ describe("unlink integration", () => { it.effect("leaves the legacy temp dir alone without reading or writing it", () => { const { layer, workdir, accessed } = setup({ stack: true }); - seedLegacyStateDecoys(workdir); - return Effect.gen(function* () { - yield* seedLinkDocument(workdir, VALID_REF); - yield* unlink(); - expect(accessed.some((entry) => entry.includes(".link.json"))).toBe(true); - expect(legacyStateAccesses(accessed)).toEqual([]); - expect(yield* tempDirExists(workdir)).toBe(true); - }).pipe(Effect.provide(layer)); + return Effect.andThen( + seedLegacyStateDecoys(workdir), + Effect.gen(function* () { + yield* seedLinkDocument(workdir, VALID_REF); + yield* unlink(); + expect(accessed.some((entry) => entry.includes(".link.json"))).toBe(true); + expect(legacyStateAccesses(accessed)).toEqual([]); + expect(yield* tempDirExists(workdir)).toBe(true); + }).pipe(Effect.provide(layer)), + ); }); it.effect("fails with ProjectRefNotLinkedError when only legacy state exists", () => { diff --git a/apps/cli/src/commands/whoami/whoami.e2e.test.ts b/apps/cli/src/commands/whoami/whoami.e2e.test.ts index 314e9b990a..7867871633 100644 --- a/apps/cli/src/commands/whoami/whoami.e2e.test.ts +++ b/apps/cli/src/commands/whoami/whoami.e2e.test.ts @@ -51,12 +51,8 @@ it.live("shows the authenticated profile through the CLI", () => }), (running) => Effect.promise(() => running.stop(true)), ); - const home = yield* Effect.acquireRelease( - Effect.try({ - try: () => makeTempHome(), - catch: (cause) => new WhoamiSetupError({ cause }), - }), - (owned) => Effect.sync(() => owned[Symbol.dispose]()), + const home = yield* makeTempHome.pipe( + Effect.mapError((cause) => new WhoamiSetupError({ cause })), ); const fs = yield* FileSystem.FileSystem; diff --git a/apps/cli/tests/e2e-sequencer.ts b/apps/cli/tests/e2e-sequencer.ts index aec487fedb..290301b9d9 100644 --- a/apps/cli/tests/e2e-sequencer.ts +++ b/apps/cli/tests/e2e-sequencer.ts @@ -1,5 +1,5 @@ -import { readFileSync } from "node:fs"; -import { relative, resolve } from "node:path"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { Data, Effect, FileSystem, Layer, Path, Schema } from "effect"; import { BaseSequencer, type TestSpecification } from "vitest/node"; /** Bun `--timings` file of measured e2e file durations, refreshed from CI artifacts. */ @@ -123,45 +123,68 @@ function isTimingsEnvelope( ); } +const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); + /** Reads the committed Bun `--timings` file under `root`; anything but that envelope reads as absent. */ -export function readTimings(root: string): Readonly> | undefined { - try { - const parsed: unknown = JSON.parse(readFileSync(resolve(root, TIMINGS_FILE), "utf8")); - return isTimingsEnvelope(parsed) ? parsed.files : undefined; - } catch { - return undefined; - } -} +export const readTimings = Effect.fn("E2eSequencer.readTimings")(function* (root: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + return yield* fs.readFile(path.resolve(root, TIMINGS_FILE)).pipe( + Effect.map((bytes) => new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes)), + Effect.flatMap(decodeJson), + Effect.map((parsed): Readonly> | undefined => + isTimingsEnvelope(parsed) ? parsed.files : undefined, + ), + Effect.orElseSucceed(() => undefined), + ); +}); + +const sequencerLayer = Layer.mergeAll(BunFileSystem.layer, BunPath.layer); + +class HashSplitFailed extends Data.TaggedError("HashSplitFailed")<{ readonly cause: unknown }> {} /** * Shards the e2e projects by measured duration. Other projects, and the e2e projects while * the timings file is absent, keep Vitest's hash split. */ export class E2eSequencer extends BaseSequencer { - override async shard(files: TestSpecification[]): Promise { + override shard(files: TestSpecification[]): Promise { const { root, shard } = this.ctx.config; - const timings = files.every((spec) => E2E_PROJECTS.has(spec.project.name)) - ? readTimings(root) - : undefined; - if (shard === undefined || timings === undefined) { - return super.shard(files); - } + const hashSplit = () => super.shard(files); + return Effect.runPromise( + Effect.gen(function* () { + const timings = files.every((spec) => E2E_PROJECTS.has(spec.project.name)) + ? yield* readTimings(root) + : undefined; + if (shard === undefined || timings === undefined) { + return yield* Effect.tryPromise({ + try: hashSplit, + catch: (cause) => new HashSplitFailed({ cause }), + }); + } - const byKey = new Map( - files.map((spec) => [relative(root, spec.moduleId).replaceAll("\\", "/"), spec]), + const path = yield* Path.Path; + const byKey = new Map( + files.map((spec) => [path.relative(root, spec.moduleId).replaceAll("\\", "/"), spec]), + ); + const shardFiles = [...byKey].map(([key, spec]) => ({ + key, + exclusive: spec.project.name === E2E_EXCLUSIVE_PROJECT, + })); + const maxWorkers = + files.find((spec) => spec.project.name === E2E_PROJECT)?.project.config.maxWorkers ?? 1; + // Concurrent files share one Docker daemon and slow each other, so pack against half the slots. + const workers = Math.max(1, Math.floor(maxWorkers / 2)); + const assigned = + packShards(shardFiles, timings, shard.count, workers)[shard.index - 1] ?? []; + return assigned.flatMap((key) => { + const spec = byKey.get(key); + return spec === undefined ? [] : [spec]; + }); + }).pipe( + Effect.provide(sequencerLayer), + Effect.catchTag("HashSplitFailed", (error) => Effect.die(error.cause)), + ), ); - const shardFiles = [...byKey].map(([key, spec]) => ({ - key, - exclusive: spec.project.name === E2E_EXCLUSIVE_PROJECT, - })); - const maxWorkers = - files.find((spec) => spec.project.name === E2E_PROJECT)?.project.config.maxWorkers ?? 1; - // Concurrent files share one Docker daemon and slow each other, so pack against half the slots. - const workers = Math.max(1, Math.floor(maxWorkers / 2)); - const assigned = packShards(shardFiles, timings, shard.count, workers)[shard.index - 1] ?? []; - return assigned.flatMap((key) => { - const spec = byKey.get(key); - return spec === undefined ? [] : [spec]; - }); } } diff --git a/apps/cli/tests/e2e-sequencer.unit.test.ts b/apps/cli/tests/e2e-sequencer.unit.test.ts index d90a7e3813..07a9467268 100644 --- a/apps/cli/tests/e2e-sequencer.unit.test.ts +++ b/apps/cli/tests/e2e-sequencer.unit.test.ts @@ -1,7 +1,7 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { it } from "@effect/vitest"; +import { Effect, FileSystem, Layer, Path, Schema } from "effect"; +import { describe, expect, test } from "vitest"; import { DEFAULT_DURATION_MS, fallbackDuration, packShards, readTimings } from "./e2e-sequencer.ts"; const files = ["src/a.e2e.test.ts", "src/b.e2e.test.ts", "src/c.e2e.test.ts", "src/d.e2e.test.ts"]; @@ -126,61 +126,83 @@ describe("fallbackDuration", () => { }); describe("readTimings", () => { - let root: string; - - beforeEach(() => { - root = mkdtempSync(join(tmpdir(), "e2e-timings-")); - mkdirSync(join(root, "tests")); - }); - - afterEach(() => { - rmSync(root, { recursive: true, force: true }); - }); - - const writeTimings = (content: string) => - writeFileSync(join(root, "tests", "e2e-timings.json"), content); - - test("reads the files map from a version 1 envelope", () => { - writeTimings( - JSON.stringify({ - version: 1, - files: { "src/a.e2e.test.ts": 1200, "src/b.e2e.test.ts": 300 }, - }), - ); - - expect(readTimings(root)).toEqual({ "src/a.e2e.test.ts": 1200, "src/b.e2e.test.ts": 300 }); - }); - - test("reads as absent when the file is missing", () => { - expect(readTimings(root)).toBeUndefined(); - }); + const fsLayer = Layer.mergeAll(BunFileSystem.layer, BunPath.layer); + const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + + const timingsRoot = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "e2e-timings-" }); + yield* fs.makeDirectory(path.join(root, "tests")); + const writeTimings = (content: string) => + fs.writeFileString(path.join(root, "tests", "e2e-timings.json"), content); + return { root, writeTimings }; + }); + + it.effect("reads the files map from a version 1 envelope", () => + Effect.gen(function* () { + const { root, writeTimings } = yield* timingsRoot; + yield* writeTimings( + yield* encodeJson({ + version: 1, + files: { "src/a.e2e.test.ts": 1200, "src/b.e2e.test.ts": 300 }, + }), + ); + + expect(yield* readTimings(root)).toEqual({ + "src/a.e2e.test.ts": 1200, + "src/b.e2e.test.ts": 300, + }); + }).pipe(Effect.provide(fsLayer)), + ); - test("reads as absent rather than half-applying when any duration is not a number", () => { - writeTimings( - JSON.stringify({ - version: 1, - files: { "src/a.e2e.test.ts": 1200, "src/b.e2e.test.ts": "300" }, - }), - ); + it.effect("reads as absent when the file is missing", () => + Effect.gen(function* () { + const { root } = yield* timingsRoot; + expect(yield* readTimings(root)).toBeUndefined(); + }).pipe(Effect.provide(fsLayer)), + ); - expect(readTimings(root)).toBeUndefined(); - }); + it.effect("reads as absent rather than half-applying when any duration is not a number", () => + Effect.gen(function* () { + const { root, writeTimings } = yield* timingsRoot; + yield* writeTimings( + yield* encodeJson({ + version: 1, + files: { "src/a.e2e.test.ts": 1200, "src/b.e2e.test.ts": "300" }, + }), + ); + + expect(yield* readTimings(root)).toBeUndefined(); + }).pipe(Effect.provide(fsLayer)), + ); - test("reads as absent when the file is not JSON", () => { - writeTimings("{ not json"); + it.effect("reads as absent when the file is not JSON", () => + Effect.gen(function* () { + const { root, writeTimings } = yield* timingsRoot; + yield* writeTimings("{ not json"); - expect(readTimings(root)).toBeUndefined(); - }); + expect(yield* readTimings(root)).toBeUndefined(); + }).pipe(Effect.provide(fsLayer)), + ); - test("reads as absent for a flat map with no envelope", () => { - writeTimings(JSON.stringify({ "src/a.e2e.test.ts": 1200, "src/b.e2e.test.ts": 300 })); + it.effect("reads as absent for a flat map with no envelope", () => + Effect.gen(function* () { + const { root, writeTimings } = yield* timingsRoot; + yield* writeTimings( + yield* encodeJson({ "src/a.e2e.test.ts": 1200, "src/b.e2e.test.ts": 300 }), + ); - expect(readTimings(root)).toBeUndefined(); - }); + expect(yield* readTimings(root)).toBeUndefined(); + }).pipe(Effect.provide(fsLayer)), + ); - test("reads as absent for an unknown envelope version", () => { - writeTimings(JSON.stringify({ version: 2, files: { "src/a.e2e.test.ts": 1200 } })); + it.effect("reads as absent for an unknown envelope version", () => + Effect.gen(function* () { + const { root, writeTimings } = yield* timingsRoot; + yield* writeTimings(yield* encodeJson({ version: 2, files: { "src/a.e2e.test.ts": 1200 } })); - expect(readTimings(root)).toBeUndefined(); - }); + expect(yield* readTimings(root)).toBeUndefined(); + }).pipe(Effect.provide(fsLayer)), + ); }); diff --git a/apps/cli/tests/e2e-setup.ts b/apps/cli/tests/e2e-setup.ts index 118c0ab7d9..6655c64db7 100644 --- a/apps/cli/tests/e2e-setup.ts +++ b/apps/cli/tests/e2e-setup.ts @@ -1,6 +1,4 @@ import { afterEach } from "vitest"; import { cleanupRegisteredStackProjects } from "./helpers/stack-e2e-cleanup.ts"; -afterEach(async () => { - await cleanupRegisteredStackProjects(); -}); +afterEach(() => cleanupRegisteredStackProjects()); diff --git a/apps/cli/tests/fixtures/compiled-cli-version.ts b/apps/cli/tests/fixtures/compiled-cli-version.ts index 07b930c371..3241498ee7 100644 --- a/apps/cli/tests/fixtures/compiled-cli-version.ts +++ b/apps/cli/tests/fixtures/compiled-cli-version.ts @@ -1,3 +1,4 @@ +import { Console, Effect } from "effect"; import { CLI_VERSION } from "../../src/shared/cli/version.ts"; -console.log(CLI_VERSION); +Effect.runSync(Console.log(CLI_VERSION)); diff --git a/apps/cli/tests/fixtures/compiled-libpg-query.ts b/apps/cli/tests/fixtures/compiled-libpg-query.ts index d421a00054..3286d1d1bc 100644 --- a/apps/cli/tests/fixtures/compiled-libpg-query.ts +++ b/apps/cli/tests/fixtures/compiled-libpg-query.ts @@ -6,6 +6,7 @@ // Bun's `--compile` traces that dynamic import inside pg-delta's dist and // still embeds the libpg-query WASM asset it ultimately depends on. import { analyzeForShadow } from "@supabase/pg-delta/sql-order"; +import { Console, Effect } from "effect"; const embeddedParser = Bun.embeddedFiles.find((file) => file.type === "application/wasm"); @@ -28,4 +29,4 @@ if (result.files.length !== 1) { throw new Error("analyzeForShadow did not reorder the expected statement"); } -console.log("libpg-query.wasm loaded"); +Effect.runSync(Console.log("libpg-query.wasm loaded")); diff --git a/apps/cli/tests/helpers/branches-live.integration.test.ts b/apps/cli/tests/helpers/branches-live.integration.test.ts index 0dd5e7cfd8..8365653d5f 100644 --- a/apps/cli/tests/helpers/branches-live.integration.test.ts +++ b/apps/cli/tests/helpers/branches-live.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { Cause, Effect, Exit, Fiber } from "effect"; +import { Cause, Data, Effect, Exit, Fiber, Schema } from "effect"; import * as TestClock from "effect/testing/TestClock"; import { @@ -31,30 +31,34 @@ type State = { deleteAttempts: number; }; +class FakeCliError extends Data.TaggedError("FakeCliError")<{ readonly message: string }> {} + +const json = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + function result(stdout = "", stderr = "", exitCode = 0) { return { stdout, stderr, exitCode }; } function statefulCli(state: State): BranchCli { return (args) => - Effect.sync(() => { + Effect.gen(function* () { state.calls.push(args.join(" ")); if (args[1] === "create") { - return result(JSON.stringify({ ...branch, project_ref: branchRef })); + return result(yield* json({ ...branch, project_ref: branchRef })); } if (args[1] === "get") { return state.getReady - ? result(JSON.stringify(branch)) + ? result(yield* json(branch)) : result("", "Request failed with status 404", 1); } if (args[1] === "list") { - return result(JSON.stringify(state.listed ? [defaultBranch, branch] : [defaultBranch])); + return result(yield* json(state.listed ? [defaultBranch, branch] : [defaultBranch])); } if (args[1] === "delete") { state.deleteAttempts += 1; if (state.deleteAttempts === 1) return result("", "Request failed with status 404", 1); state.listed = false; - return result(JSON.stringify(branch)); + return result(yield* json(branch)); } return result("", "unexpected command", 1); }); @@ -108,13 +112,13 @@ describe("live branch lifecycle helpers", () => { Effect.gen(function* () { const state: State = { calls: [], listed: true, getReady: true, deleteAttempts: 0 }; const cli: BranchCli = (args) => - Effect.sync(() => { + Effect.gen(function* () { state.calls.push(args.join(" ")); if (args[1] === "list") { if (state.calls.filter((call) => call.includes("branches list")).length > 1) { state.listed = false; } - return result(JSON.stringify(state.listed ? [defaultBranch, branch] : [defaultBranch])); + return result(yield* json(state.listed ? [defaultBranch, branch] : [defaultBranch])); } return result(); }); @@ -146,9 +150,9 @@ describe("live branch lifecycle helpers", () => { Effect.gen(function* () { let attempts = 0; const cli: BranchCli = () => - Effect.sync(() => { + Effect.gen(function* () { attempts += 1; - return result(JSON.stringify([{}])); + return result(yield* json([{}])); }); const exit = yield* Effect.exit(awaitLiveBranchListedEffect(cli, project, branch.name)); if (!Exit.isFailure(exit)) throw new Error("expected malformed list failure"); @@ -174,16 +178,16 @@ describe("live branch lifecycle helpers", () => { Effect.gen(function* () { const state: State = { calls: [], listed: true, getReady: false, deleteAttempts: 0 }; const cli: BranchCli = (args) => - Effect.sync(() => { + Effect.gen(function* () { state.calls.push(args.join(" ")); if (args[1] === "create") - return result(JSON.stringify({ message: "Created preview branch" })); + return result(yield* json({ message: "Created preview branch" })); if (args[1] === "delete") { state.listed = false; return result(); } if (args[1] === "list") - return result(JSON.stringify(state.listed ? [defaultBranch, branch] : [defaultBranch])); + return result(yield* json(state.listed ? [defaultBranch, branch] : [defaultBranch])); return result(); }); const exit = yield* Effect.exit(createLiveBranchEffect(cli, project, branch.name)); @@ -200,11 +204,11 @@ describe("live branch lifecycle helpers", () => { Effect.gen(function* () { const state: State = { calls: [], listed: true, getReady: false, deleteAttempts: 0 }; const cli: BranchCli = (args) => - Effect.sync(() => { + Effect.gen(function* () { state.calls.push(args.join(" ")); if (args[1] === "create") return result( - JSON.stringify({ _tag: "Error", error: { message: "branch quota exceeded" } }), + yield* json({ _tag: "Error", error: { message: "branch quota exceeded" } }), "", 1, ); @@ -213,7 +217,7 @@ describe("live branch lifecycle helpers", () => { return result(); } if (args[1] === "list") - return result(JSON.stringify(state.listed ? [defaultBranch, branch] : [defaultBranch])); + return result(yield* json(state.listed ? [defaultBranch, branch] : [defaultBranch])); return result(); }); const exit = yield* Effect.exit(createLiveBranchEffect(cli, project, branch.name)); @@ -233,14 +237,14 @@ describe("live branch lifecycle helpers", () => { const state: State = { calls: [], listed: false, getReady: false, deleteAttempts: 0 }; let creates = 0; const cli: BranchCli = (args) => - Effect.sync(() => { + Effect.gen(function* () { state.calls.push(args.join(" ")); if (args[1] === "create") { creates += 1; state.listed = true; return creates === 1 ? result( - JSON.stringify({ + yield* json({ _tag: "Error", error: { message: @@ -250,16 +254,14 @@ describe("live branch lifecycle helpers", () => { "", 1, ) - : result(JSON.stringify(branch)); + : result(yield* json(branch)); } if (args[1] === "delete") { state.listed = false; return result(); } if (args[1] === "list") - return result( - JSON.stringify(state.listed ? [defaultBranch, branch] : [defaultBranch]), - ); + return result(yield* json(state.listed ? [defaultBranch, branch] : [defaultBranch])); return result(); }); const ref = yield* createLiveBranchEffect(cli, project, branch.name); @@ -279,12 +281,12 @@ describe("live branch lifecycle helpers", () => { Effect.gen(function* () { let creates = 0; const cli: BranchCli = (args) => - Effect.sync(() => { + Effect.gen(function* () { if (args[1] === "create") { creates += 1; return result("", "status 400: the database system is shutting down", 1); } - return result(JSON.stringify([defaultBranch])); + return result(yield* json([defaultBranch])); }); const exit = yield* Effect.exit(createLiveBranchEffect(cli, project, branch.name)); if (!Exit.isFailure(exit)) throw new Error("expected create failure"); @@ -317,7 +319,7 @@ describe("live branch lifecycle helpers", () => { const exit = yield* Effect.exit(awaitLiveBranchEffect(cli, project, branch.name)); if (!Exit.isFailure(exit)) throw new Error("expected get failure"); expect(String(exit.cause)).not.toContain("sentinel-secret"); - expect(JSON.stringify(Cause.squash(exit.cause))).not.toContain("sentinel-secret"); + expect(yield* json(Cause.squash(exit.cause))).not.toContain("sentinel-secret"); }), ); @@ -326,9 +328,10 @@ describe("live branch lifecycle helpers", () => { const calls: string[] = []; const cli: BranchCli = (args) => { calls.push(args.join(" ")); - if (args[1] === "create") return Effect.fail(new Error("invocation failed")); + if (args[1] === "create") + return Effect.fail(new FakeCliError({ message: "invocation failed" })); if (args[1] === "delete") return Effect.succeed(result()); - if (args[1] === "list") return Effect.succeed(result(JSON.stringify([defaultBranch]))); + if (args[1] === "list") return json([defaultBranch]).pipe(Effect.map(result)); return Effect.succeed(result()); }; const exit = yield* Effect.exit(createLiveBranchEffect(cli, project, branch.name)); @@ -342,7 +345,7 @@ describe("live branch lifecycle helpers", () => { Effect.gen(function* () { const cli: BranchCli = (args) => args[1] === "create" - ? Effect.fail(new Error("create invocation failed")) + ? Effect.fail(new FakeCliError({ message: "create invocation failed" })) : Effect.succeed(result("", "cleanup forbidden", 1)); const exit = yield* Effect.exit(createLiveBranchEffect(cli, project, branch.name)); if (!Exit.isFailure(exit)) throw new Error("expected aggregate failure"); @@ -359,10 +362,10 @@ describe("live branch lifecycle helpers", () => { Effect.gen(function* () { const cli: BranchCli = (args) => args[1] === "create" - ? Effect.fail(new Error("create invocation failed")) + ? Effect.fail(new FakeCliError({ message: "create invocation failed" })) : args[1] === "delete" ? Effect.succeed(result("", "Request failed with status 404", 1)) - : Effect.succeed(result(JSON.stringify([defaultBranch]))); + : json([defaultBranch]).pipe(Effect.map(result)); const cleanup = yield* createLiveBranchEffect(cli, project, branch.name).pipe( Effect.forkChild({ startImmediately: true }), ); @@ -382,7 +385,7 @@ describe("live branch lifecycle helpers", () => { const cli: BranchCli = (args) => args[1] === "delete" ? Effect.sleep("60 seconds").pipe(Effect.as(result())) - : Effect.succeed(result(JSON.stringify([defaultBranch, branch]))); + : json([defaultBranch, branch]).pipe(Effect.map(result)); const waiting = yield* awaitLiveBranchRemovedEffect(cli, project, branchRef).pipe( Effect.forkChild({ startImmediately: true }), ); diff --git a/apps/cli/tests/helpers/cli.ts b/apps/cli/tests/helpers/cli.ts index e02bdb2b14..e373e336c9 100644 --- a/apps/cli/tests/helpers/cli.ts +++ b/apps/cli/tests/helpers/cli.ts @@ -1,13 +1,25 @@ +import { BunServices } from "@effect/platform-bun"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; -import { spawn } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs"; -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import type * as PlatformError from "effect/PlatformError"; import { createServer } from "node:net"; import { homedir, tmpdir } from "node:os"; -import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; -import { Data, Effect, Exit, Stream } from "effect"; +import { + Config, + Data, + Deferred, + Effect, + Exit, + Fiber, + FileSystem, + Path, + Predicate, + PubSub, + Schema, + Scope, + Stream, +} from "effect"; import { noteStackCliProjectHome, registerTempHome, @@ -56,16 +68,6 @@ function subprocessBaseEnv(): Record { return env; } -function assertBuildArtifactsExist(binaryPath: string): void { - if (!existsSync(SHIM_PATH) || !existsSync(binaryPath)) { - throw new Error( - `Missing CLI build artifacts. Run \`pnpm --filter supabase build\` before invoking e2e tests.\n` + - ` expected shim: ${SHIM_PATH}\n` + - ` expected binary: ${binaryPath}`, - ); - } -} - export type RunResult = { stdout: string; stderr: string; @@ -92,6 +94,11 @@ class CliSpawnError extends Data.TaggedError("CliSpawnError")<{ } } +/** The compiled CLI is missing, so there is nothing to spawn. */ +class CliBuildArtifactsError extends Data.TaggedError("CliBuildArtifactsError")<{ + readonly message: string; +}> {} + /** Disposing a run-owned or test-owned temp `SUPABASE_HOME` failed. */ export class CliHomeDisposeError extends Data.TaggedError("CliHomeDisposeError")<{ readonly cause: unknown; @@ -103,75 +110,144 @@ export class CliHomeDisposeError extends Data.TaggedError("CliHomeDisposeError") export type CliRunError = CliSpawnError | CliStdinWriteError | CliHomeDisposeError; +/** The CLI exited, or the wait timed out, before its stdout matched. */ +class CliOutputWaitError extends Data.TaggedError("CliOutputWaitError")<{ + readonly message: string; +}> {} + +/** No free port could be allocated for this e2e worker. */ +class E2ePortError extends Data.TaggedError("E2ePortError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +/** Stopping the stack of an owned temp project failed. */ +class TempStackCleanupError extends Data.TaggedError("TempStackCleanupError")<{ + readonly message: string; +}> {} + +const helperServices = BunServices.layer; + +/* UTF-8 decoding that keeps a leading byte order mark, like Node's `Buffer#toString`. */ +const chunkDecoder = new TextDecoder("utf-8", { ignoreBOM: true }); + +/** The host error a platform failure carries, which is what the file system call raised. */ +const hostCause = (error: PlatformError.PlatformError): unknown => error.cause ?? error; + +/* A platform or port failure's host error, else a plain `Error` with the failure's message. */ +function hostOrPlainError(error: Error): Error { + if (Predicate.isTagged(error, "PlatformError") || error instanceof E2ePortError) { + return error.cause instanceof Error ? error.cause : new Error(error.message); + } + if ( + error instanceof TempStackCleanupError || + error instanceof CliOutputWaitError || + error instanceof CliBuildArtifactsError + ) { + return new Error(error.message); + } + return error; +} + +/** + * What a Promise helper rejects with: the host error a platform or port failure carries, else a + * plain `Error` with the failure's message. A home disposal failure stays typed. + */ +export function helperRejection(error: Error): Error { + if (error instanceof CliHomeDisposeError) { + return error; + } + if (error instanceof CliSpawnError || error instanceof CliStdinWriteError) { + return error.cause instanceof Error ? hostOrPlainError(error.cause) : new Error(error.message); + } + return hostOrPlainError(error); +} + +/** Promise edge for the helpers non-Effect tests `await`. */ +const runHelper = ( + effect: Effect.Effect, +): Promise => + Effect.runPromise(effect.pipe(Effect.mapError(helperRejection), Effect.provide(helperServices))); + +const removeOwnedDir = (dir: string) => + FileSystem.FileSystem.use((fs) => fs.remove(dir, { recursive: true, force: true })); + +/** Like `existsSync`: any failure to reach the path reads as "absent". */ +const pathExists = (fs: FileSystem.FileSystem, pathname: string) => + fs.exists(pathname).pipe(Effect.orElseSucceed(() => false)); + const DEFAULT_EXIT_TIMEOUT_MS = 60_000; const DEFAULT_STACK_CLEANUP_TIMEOUT_MS = 120_000; const OUTPUT_TAIL_LENGTH = 4_000; -interface SpawnedSupabase { - readonly pid: number; - readonly homeDir: string; - readonly stdout: () => string; - readonly stderr: () => string; - readonly kill: (signal?: NodeJS.Signals) => void; - readonly waitForOutput: (pattern: RegExp, timeoutMs?: number, startAt?: number) => Promise; - readonly waitForExit: (timeoutMs?: number) => Promise; - /** Effect-native exit path; `waitForExit` is the Promise facade over this. */ - readonly exitEffect: (timeoutMs?: number) => Effect.Effect; - /** The deferred stdin write failure, if the CLI closed stdin early. */ - readonly stdinFailure: (result: RunResult) => Error | undefined; - /** - * Effect-path scope release: kills the group once (unless the caller opted out on a - * successful run), then disposes the owned temp home. Idempotent across the close path. - */ - readonly releaseOwned: (opts: { readonly successOptOut: boolean }) => void; +/** A temp `SUPABASE_HOME`; `dispose` removes it (repeatably) and keeps failures typed. */ +export interface TempHome { + readonly dir: string; + readonly dispose: Effect.Effect; } -export function makeTempHome() { - const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; - const dir = mkdtempSync(path.join(tempRoot, "sb-test-")); +/** The run's owned temp `SUPABASE_HOME` could not be created. */ +export class TempHomeSetupError extends Data.TaggedError("TempHomeSetupError")<{ + readonly message: string; + readonly cause: unknown; +}> {} + +const createTempHomeEffect = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectory({ + directory: process.platform === "win32" ? tmpdir() : "/tmp", + prefix: "sb-test-", + }); // Share the real binary cache so tests don't re-download binaries. const realBinDir = path.join(homedir(), ".supabase", "bin"); - if (existsSync(realBinDir)) { - mkdirSync(dir, { recursive: true }); - symlinkSync(realBinDir, path.join(dir, "bin")); + if (yield* pathExists(fs, realBinDir)) { + yield* fs.makeDirectory(dir, { recursive: true }); + yield* fs.symlink(realBinDir, path.join(dir, "bin")); } - const home = { - dir, - [Symbol.dispose]() { - rmSync(dir, { recursive: true, force: true }); - }, - }; - registerTempHome(home); + const context = yield* Effect.context(); + const dispose = removeOwnedDir(dir).pipe( + Effect.mapError((cause) => new CliHomeDisposeError({ cause: hostCause(cause) })), + Effect.provide(context), + ); + const home: TempHome = { dir, dispose }; + yield* Effect.sync(() => + registerTempHome({ + dir, + dispose: () => + Effect.runPromiseWith(context)(removeOwnedDir(dir).pipe(Effect.mapError(helperRejection))), + }), + ); return home; -} +}); -/** The run's owned temp `SUPABASE_HOME` could not be created. */ -export class TempHomeSetupError extends Data.TaggedError("TempHomeSetupError")<{ - readonly message: string; - readonly cause: unknown; -}> {} +/** + * Creates and registers a temp `SUPABASE_HOME` without a release: the caller disposes it, or + * the e2e cleanup drain does when a stack project uses it. + */ +export const createTempHome: Effect.Effect = + createTempHomeEffect.pipe( + Effect.mapError( + (cause) => + new TempHomeSetupError({ message: "temp home setup failed", cause: hostCause(cause) }), + ), + Effect.provide(helperServices), + ); + +/** A temp `SUPABASE_HOME` owned by the calling scope, removed when the scope closes. */ +export const makeTempHome: Effect.Effect = + Effect.acquireRelease(createTempHome, (home) => Effect.orDie(home.dispose)); /** * Runs `use` with an owned temp `SUPABASE_HOME`. Setup and disposal failures both stay in - * the typed channel (`rmSync` can throw), which a scoped release could not express. + * the typed channel, which a scoped release could not express. */ export const withTempHome = ( - use: (home: ReturnType) => Effect.Effect, + use: (home: TempHome) => Effect.Effect, ): Effect.Effect => - Effect.acquireUseRelease( - Effect.try({ - try: () => makeTempHome(), - catch: (cause) => new TempHomeSetupError({ message: "temp home setup failed", cause }), - }), - use, - (owned) => - Effect.try({ - try: () => owned[Symbol.dispose](), - catch: (cause) => new CliHomeDisposeError({ cause }), - }), - ); + Effect.acquireUseRelease(createTempHome, use, (owned) => owned.dispose); /** The run's owned temp CLI project could not be created or removed. */ export class TempCliProjectError extends Data.TaggedError("TempCliProjectError")<{ @@ -188,56 +264,105 @@ export const withTempCliProject = ( use: (project: Awaited>) => Effect.Effect, ): Effect.Effect => Effect.acquireUseRelease( - Effect.tryPromise({ - try: () => makeTempCliProject(prefix), - catch: (cause) => - new TempCliProjectError({ message: "temp CLI project setup failed", cause }), - }), + makeTempCliProjectEffect(prefix).pipe( + Effect.provide(helperServices), + Effect.mapError( + (cause) => + new TempCliProjectError({ + message: "temp CLI project setup failed", + cause: hostCause(cause), + }), + ), + ), use, (project) => - Effect.tryPromise({ - try: () => project.cleanup(), - catch: (cause) => - new TempCliProjectError({ message: "temp CLI project cleanup failed", cause }), - }), + removeOwnedDir(project.dir).pipe( + Effect.provide(helperServices), + Effect.mapError( + (cause) => + new TempCliProjectError({ + message: "temp CLI project cleanup failed", + cause: hostCause(cause), + }), + ), + ), ); +const portError = (error: Error) => new E2ePortError({ message: error.message, cause: error }); + +const pickReleasedPort = Effect.callback((resume) => { + const server = createServer(); + let settled = false; + const settle = (result: Effect.Effect) => { + if (settled) return; + settled = true; + resume(result); + }; + /* Stays attached after cancellation so a late listen error is never an unhandled event. */ + server.on("error", (error) => settle(Effect.fail(portError(error)))); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (address == null || typeof address === "string") { + server.close(() => + settle(Effect.fail(new E2ePortError({ message: "Failed to allocate a free port" }))), + ); + return; + } + server.close((error) => { + settle(error ? Effect.fail(portError(error)) : Effect.succeed(address.port)); + }); + }); + return Effect.sync(() => { + settled = true; + server.close(); + }); +}); + /** * Concurrent e2e workers each keep only ports in their own slot, so a port one worker picked and * released before Docker binds it is never handed to another worker. Slots skip the low bit * because Linux hands out mostly odd ports for `listen(0)`. */ -const e2eWorkers = Number(process.env.SUPABASE_E2E_WORKERS || "1"); -const e2eWorkerSlot = Number(process.env.VITEST_POOL_ID || "1") % e2eWorkers; +const envOrEmpty = (name: string) => Config.String(name).pipe(Config.withDefault("")); -async function pickFreePort(): Promise { +const pickFreePort = Effect.gen(function* () { + const e2eWorkers = Number((yield* envOrEmpty("SUPABASE_E2E_WORKERS")) || "1"); + const e2eWorkerSlot = Number((yield* envOrEmpty("VITEST_POOL_ID")) || "1") % e2eWorkers; for (let attempt = 0; attempt < 1000; attempt++) { - const port = await pickReleasedPort(); + const port = yield* pickReleasedPort; if (Math.floor(port / 2) % e2eWorkers === e2eWorkerSlot) return port; } - throw new Error(`No free port for e2e worker slot ${e2eWorkerSlot} of ${e2eWorkers}`); -} + return yield* new E2ePortError({ + message: `No free port for e2e worker slot ${e2eWorkerSlot} of ${e2eWorkers}`, + }); +}); -function pickReleasedPort(): Promise { - return new Promise((resolve, reject) => { - const server = createServer(); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (address == null || typeof address === "string") { - server.close(() => reject(new Error("Failed to allocate a free port"))); - return; +const overrideStackPortsEffect = (projectDir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + const configPath = pathService.join(projectDir, "supabase", "config.toml"); + /* Decoded keeping a leading byte order mark, like Node's `Buffer#toString`. */ + const config = chunkDecoder.decode(yield* fs.readFile(configPath)); + const assigned = new Set(); + const lines: string[] = []; + for (const line of config.split("\n")) { + const match = /^(\s*(?:port|smtp_port|pop3_port|inspector_port|shadow_port) = )\d+$/.exec( + line, + ); + if (match === null) { + lines.push(line); + continue; } - server.close((error) => { - if (error) { - reject(error); - return; - } - resolve(address.port); - }); - }); - server.on("error", reject); + let port = yield* pickFreePort; + while (assigned.has(port)) { + port = yield* pickFreePort; + } + assigned.add(port); + lines.push(`${match[1]}${port}`); + } + yield* fs.writeFileString(configPath, lines.join("\n")); }); -} /** * Rewrites every active port assignment in an `init`-generated @@ -245,152 +370,166 @@ function pickReleasedPort(): Promise { * from default configs cannot collide on host ports with other e2e stacks on * the same runner. Commented-out port lines are left untouched. */ -export async function overrideStackPorts(projectDir: string) { - const configPath = path.join(projectDir, "supabase", "config.toml"); - const config = await readFile(configPath, "utf8"); - const assigned = new Set(); - const lines: string[] = []; - for (const line of config.split("\n")) { - const match = /^(\s*(?:port|smtp_port|pop3_port|inspector_port|shadow_port) = )\d+$/.exec(line); - if (match === null) { - lines.push(line); - continue; - } - let port = await pickFreePort(); - while (assigned.has(port)) { - port = await pickFreePort(); - } - assigned.add(port); - lines.push(`${match[1]}${port}`); - } - await writeFile(configPath, lines.join("\n")); +export function overrideStackPorts(projectDir: string): Promise { + return runHelper(overrideStackPortsEffect(projectDir)); } -async function makeTempProject(prefix = "supabase-project-e2e-") { - const projectDir = await mkdtemp(path.join(tmpdir(), prefix)); +const makeTempProjectEffect = (prefix = "supabase-project-e2e-") => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const context = yield* Effect.context(); + const projectDir = yield* fs.makeTempDirectory({ prefix }); + + return { + dir: projectDir, + cleanup: (): Promise => + Effect.runPromiseWith(context)( + removeOwnedDir(projectDir).pipe(Effect.mapError(helperRejection)), + ), + }; + }); - return { - dir: projectDir, - async cleanup() { - await rm(projectDir, { recursive: true, force: true }); - }, - }; -} +const makeTempCliProjectEffect = (prefix = "supabase-cli-e2e-") => + makeTempProjectEffect(prefix).pipe( + Effect.tap((project) => Effect.sync(() => registerTempStackProject(project))), + ); /** Create an isolated CLI project without pre-allocating released ports. */ -export async function makeTempCliProject(prefix = "supabase-cli-e2e-") { - const project = await makeTempProject(prefix); - registerTempStackProject(project); - return project; +export function makeTempCliProject(prefix?: string) { + return runHelper(makeTempCliProjectEffect(prefix)); } -export async function makeTempCliStackProject( - prefix = "supabase-stack-e2e-", - cleanupTimeoutMs = DEFAULT_STACK_CLEANUP_TIMEOUT_MS, -) { - const project = await makeTempProject(prefix); - const cleanup = async () => { - if (!existsSync(project.dir)) return; +const stopAndRemoveStackProject = (dir: string, cleanupTimeoutMs: number) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + if (!(yield* pathExists(fs, dir))) return; // `init` can fail before creating a project config. There is no stack to // stop in that case, so remove the exact owned directory directly. - if (!existsSync(path.join(project.dir, "supabase", "config.toml"))) { - await rm(project.dir, { recursive: true, force: true }); + if (!(yield* pathExists(fs, pathService.join(dir, "supabase", "config.toml")))) { + yield* removeOwnedDir(dir); return; } - const stopped = await runSupabase(["stop", "--no-backup"], { - cwd: project.dir, + const stopped = yield* runSupabaseEffect(["stop", "--no-backup"], { + cwd: dir, exitTimeoutMs: cleanupTimeoutMs, }); if (stopped.exitCode !== 0) { - throw new Error( - [ - `Failed to stop stack in ${project.dir} (exit code ${stopped.exitCode}).`, + return yield* new TempStackCleanupError({ + message: [ + `Failed to stop stack in ${dir} (exit code ${stopped.exitCode}).`, `stdout:\n${stopped.stdout}`, `stderr:\n${stopped.stderr}`, ].join("\n"), - ); + }); } - await rm(project.dir, { recursive: true, force: true }); - }; + yield* removeOwnedDir(dir); + }); - const stackProject = { dir: project.dir, cleanup }; - registerTempStackProject(stackProject); - return stackProject; +const makeTempCliStackProjectEffect = (prefix: string, cleanupTimeoutMs: number) => + Effect.gen(function* () { + const context = yield* Effect.context(); + const project = yield* makeTempProjectEffect(prefix); + const stackProject = { + dir: project.dir, + cleanup: (): Promise => + Effect.runPromiseWith(context)( + stopAndRemoveStackProject(project.dir, cleanupTimeoutMs).pipe( + Effect.mapError(helperRejection), + ), + ), + }; + yield* Effect.sync(() => registerTempStackProject(stackProject)); + return stackProject; + }); + +export function makeTempCliStackProject( + prefix = "supabase-stack-e2e-", + cleanupTimeoutMs = DEFAULT_STACK_CLEANUP_TIMEOUT_MS, +) { + return runHelper(makeTempCliStackProjectEffect(prefix, cleanupTimeoutMs)); } /** Concurrent e2e files each run a stack, and Docker names its containers after the project id. */ -const projectIdFor = (dir: string) => - `e2e-${path +const projectIdFor = (pathService: Path.Path, dir: string) => + `e2e-${pathService .basename(dir) .toLowerCase() .replace(/[^a-z0-9]/g, "") .slice(-8)}`; -export async function makeTempStackProject(prefix = "supabase-stack-e2e-") { - const project = await makeTempProject(prefix); - const ports = { - apiPort: await pickFreePort(), - dbPort: await pickFreePort(), - authPort: await pickFreePort(), - postgrestPort: await pickFreePort(), - postgrestAdminPort: await pickFreePort(), - edgeRuntimePort: await pickFreePort(), - edgeRuntimeInspectorPort: await pickFreePort(), - realtimePort: await pickFreePort(), - storagePort: await pickFreePort(), - imgproxyPort: await pickFreePort(), - mailpitPort: await pickFreePort(), - mailpitSmtpPort: await pickFreePort(), - mailpitPop3Port: await pickFreePort(), - pgmetaPort: await pickFreePort(), - studioPort: await pickFreePort(), - analyticsPort: await pickFreePort(), - poolerPort: await pickFreePort(), - poolerApiPort: await pickFreePort(), - }; +const makeTempStackProjectEffect = (prefix: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + const project = yield* makeTempProjectEffect(prefix); + const ports = { + apiPort: yield* pickFreePort, + dbPort: yield* pickFreePort, + authPort: yield* pickFreePort, + postgrestPort: yield* pickFreePort, + postgrestAdminPort: yield* pickFreePort, + edgeRuntimePort: yield* pickFreePort, + edgeRuntimeInspectorPort: yield* pickFreePort, + realtimePort: yield* pickFreePort, + storagePort: yield* pickFreePort, + imgproxyPort: yield* pickFreePort, + mailpitPort: yield* pickFreePort, + mailpitSmtpPort: yield* pickFreePort, + mailpitPop3Port: yield* pickFreePort, + pgmetaPort: yield* pickFreePort, + studioPort: yield* pickFreePort, + analyticsPort: yield* pickFreePort, + poolerPort: yield* pickFreePort, + poolerApiPort: yield* pickFreePort, + }; + + const supabaseDir = pathService.join(project.dir, "supabase"); + yield* fs.makeDirectory(supabaseDir, { recursive: true }); + yield* fs.writeFileString( + pathService.join(supabaseDir, "config.toml"), + [ + `project_id = "${projectIdFor(pathService, project.dir)}"`, + "", + "[api]", + `port = ${ports.apiPort}`, + "", + "[db]", + `port = ${ports.dbPort}`, + "", + "[db.pooler]", + `port = ${ports.poolerPort}`, + "", + "[edge_runtime]", + `inspector_port = ${ports.edgeRuntimeInspectorPort}`, + "", + "[local_smtp]", + `port = ${ports.mailpitPort}`, + `smtp_port = ${ports.mailpitSmtpPort}`, + `pop3_port = ${ports.mailpitPop3Port}`, + "", + "[studio]", + `port = ${ports.studioPort}`, + "", + "[analytics]", + `port = ${ports.analyticsPort}`, + "", + ].join("\n"), + ); - const supabaseDir = path.join(project.dir, "supabase"); - await mkdir(supabaseDir, { recursive: true }); - await writeFile( - path.join(supabaseDir, "config.toml"), - [ - `project_id = "${projectIdFor(project.dir)}"`, - "", - "[api]", - `port = ${ports.apiPort}`, - "", - "[db]", - `port = ${ports.dbPort}`, - "", - "[db.pooler]", - `port = ${ports.poolerPort}`, - "", - "[edge_runtime]", - `inspector_port = ${ports.edgeRuntimeInspectorPort}`, - "", - "[local_smtp]", - `port = ${ports.mailpitPort}`, - `smtp_port = ${ports.mailpitSmtpPort}`, - `pop3_port = ${ports.mailpitPop3Port}`, - "", - "[studio]", - `port = ${ports.studioPort}`, - "", - "[analytics]", - `port = ${ports.analyticsPort}`, - "", - ].join("\n"), - ); + const stackProject = { + ...project, + ports, + }; + yield* Effect.sync(() => registerTempStackProject(stackProject)); + return stackProject; + }); - const stackProject = { - ...project, - ports, - }; - registerTempStackProject(stackProject); - return stackProject; +export function makeTempStackProject(prefix = "supabase-stack-e2e-") { + return runHelper(makeTempStackProjectEffect(prefix)); } /** Send a signal to the process group led by `pid`. */ @@ -400,6 +539,13 @@ function killProcessGroup(pid: number, signal: NodeJS.Signals): void { } catch {} } +/** Send a signal to the process `pid` alone. */ +function killProcess(pid: number, signal: NodeJS.Signals): void { + try { + process.kill(pid, signal); + } catch {} +} + function outputTail(label: string, output: string): string { if (output.length === 0) { return `${label}: `; @@ -410,33 +556,91 @@ function outputTail(label: string, output: string): string { return `${label}:\n${tail}`; } -export function spawnSupabase( - args: string[], - options?: { - cwd?: string; - /** `undefined` REMOVES the key from the child env (base env and pins included). */ - env?: Record; - /** Reuse a temp SUPABASE_HOME directory instead of creating a new one per call. */ - home?: string; - /** Write this string to stdin, then close it. */ - stdin?: string; - /** Whether to kill the whole process group once the root process exits. */ - cleanupProcessGroupOnClose?: boolean; - /** Maximum time to wait for the process to exit before force-killing it. */ - exitTimeoutMs?: number; - /** Close the read end of stdout at spawn, like a `| head` reader that has already exited. */ - closeStdout?: boolean; - }, -): SpawnedSupabase { - const ownHome = options?.home ? null : makeTempHome(); - const homeDir = options?.home ?? ownHome!.dir; - noteStackCliProjectHome(options?.cwd, homeDir); +export interface SpawnSupabaseOptions { + readonly cwd?: string; + /** `undefined` REMOVES the key from the child env (base env and pins included). */ + readonly env?: Record; + /** Reuse a temp SUPABASE_HOME directory instead of creating a new one per call. */ + readonly home?: string; + /** Write this string to stdin, then close it. */ + readonly stdin?: string; + /** Whether to kill the whole process group once the root process exits. */ + readonly cleanupProcessGroupOnClose?: boolean; + /** Maximum time to wait for the process to exit before force-killing it. */ + readonly exitTimeoutMs?: number; + /** Close the read end of stdout at spawn, like a `| head` reader that has already exited. */ + readonly closeStdout?: boolean; +} + +export interface SpawnedSupabase { + readonly pid: number; + readonly homeDir: string; + readonly stdout: () => string; + readonly stderr: () => string; + /** Signals the process group, then the root process while it still runs. */ + readonly kill: (signal?: NodeJS.Signals) => Effect.Effect; + readonly waitForOutput: ( + pattern: RegExp, + timeoutMs?: number, + startAt?: number, + ) => Effect.Effect; + /** + * The single exit path: waits for exit and drained output within the exit bound (SIGKILLing + * the group when it fires), kills the group, disposes the owned home, and surfaces a deferred + * stdin write failure. + */ + readonly waitForExit: ( + timeoutMs?: number, + ) => Effect.Effect; +} + +const isBrokenPipe = (error: { readonly cause?: unknown }) => + Predicate.hasProperty(error.cause, "code") && error.cause.code === "EPIPE"; + +/* + * The Effect spawner only hands out stdout as a stream it reads itself, so a pipe whose reader + * has exited needs a relay: it runs the CLI on a real pipe, closes the read end at once, and + * mirrors the CLI's exit code. It ignores group signals so the CLI's own exit wins. + */ +const closedStdoutRelay = (argvJson: string) => + [ + `const { spawn } = require("node:child_process");`, + `const child = spawn(process.execPath, ${argvJson}, { stdio: ["inherit", "pipe", "inherit"] });`, + `child.stdout.destroy();`, + `for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => {});`, + `child.on("exit", (code) => process.exit(code ?? 1));`, + ].join("\n"); + +const encodeArgv = Schema.encodeEffect(Schema.fromJsonString(Schema.Array(Schema.String))); + +const spawnSupabaseProgram = Effect.fnUntraced(function* ( + args: ReadonlyArray, + options: SpawnSupabaseOptions | undefined, +) { + const fs = yield* FileSystem.FileSystem; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + let disposedOwnHome = false; + const disposeHome = (home: TempHome) => + Effect.suspend(() => { + if (disposedOwnHome) { + return Effect.void; + } + disposedOwnHome = true; + return home.dispose; + }); + /* The run-owned home's removal is registered with its creation, so cancellation never leaks it; + as the first release it runs last, after the child is killed. */ + const ownHome = options?.home + ? undefined + : yield* Effect.acquireRelease( + createTempHomeEffect.pipe( + Effect.mapError((cause) => new CliSpawnError({ cause: hostCause(cause) })), + ), + (home) => Effect.orDie(disposeHome(home)), + ); + const homeDir = options?.home ?? ownHome?.dir ?? ""; + yield* Effect.sync(() => noteStackCliProjectHome(options?.cwd, homeDir)); const usesStartWrapper = args[0] === "start"; - // Exercise the same shim + compiled shell binary handoff that published - // packages use. `SUPABASE_CLI_BINARY_OVERRIDE` points the shim at the local - // build artifact without needing platform wrapper packages. - let execCmd: string; - let execArgs: string[]; // An `undefined` in `options.env` removes the key entirely — pins and ambient values alike — // so a cache-subject test can run with a variable genuinely absent (the shipped default), // not just overridden. @@ -454,76 +658,138 @@ export function spawnSupabase( for (const [key, value] of Object.entries(mergedEnv)) { if (value !== undefined) env[key] = value; } - assertBuildArtifactsExist(BINARY_PATH); + const artifactsExist = (yield* pathExists(fs, SHIM_PATH)) && (yield* pathExists(fs, BINARY_PATH)); + if (!artifactsExist) { + return yield* new CliBuildArtifactsError({ + message: + `Missing CLI build artifacts. Run \`pnpm --filter supabase build\` before invoking e2e tests.\n` + + ` expected shim: ${SHIM_PATH}\n` + + ` expected binary: ${BINARY_PATH}`, + }); + } + // Exercise the same shim + compiled shell binary handoff that published + // packages use. `SUPABASE_CLI_BINARY_OVERRIDE` points the shim at the local + // build artifact without needing platform wrapper packages. env["SUPABASE_CLI_BINARY_OVERRIDE"] = BINARY_PATH; - execCmd = "node"; - execArgs = [SHIM_PATH, ...args]; - const proc = spawn(execCmd, execArgs, { + const shimArgs = [SHIM_PATH, ...args]; + const commandArgs = options?.closeStdout + ? ["-e", closedStdoutRelay(yield* encodeArgv(shimArgs))] + : shimArgs; + const command = ChildProcess.make("node", commandArgs, { cwd: options?.cwd, env, - stdio: - usesStartWrapper || options?.stdin !== undefined - ? ["pipe", "pipe", "pipe"] - : ["ignore", "pipe", "pipe"], + extendEnv: false, + stdin: usesStartWrapper || options?.stdin !== undefined ? "pipe" : "ignore", + stdout: "pipe", + stderr: "pipe", // Own process group so tests can distinguish product cleanup from helper cleanup. detached: true, + /* The harness owns every group kill. The spawner signals the group itself on a nonzero exit + and at release; SIGURG is ignored by default and leaves stopped processes stopped. */ + killSignal: "SIGURG", }); - const stdoutStream = proc.stdout; - const stderrStream = proc.stderr; - if (stdoutStream == null || stderrStream == null) { - throw new Error("Expected spawned Supabase process to expose stdout/stderr pipes"); - } - if (options?.closeStdout) stdoutStream.destroy(); + let closeResult: RunResult | undefined; + let cleanedUpProcessGroup = false; + const signalGroup = (handle: ChildProcessSpawner.ChildProcessHandle, name: NodeJS.Signals) => + Effect.flatMap( + Effect.orElseSucceed(handle.isRunning, () => false), + (running) => + Effect.sync(() => { + killProcessGroup(handle.pid, name); + if (running) killProcess(handle.pid, name); + }), + ); + + // Scope release owns the spawned group: a run that did not complete always kills it so the + // child is never orphaned, without re-signaling a group the close path already cleaned. On + // successful completion the caller's `cleanupProcessGroupOnClose: false` opt-out is honoured. + const handle = yield* Effect.acquireRelease(spawner.spawn(command), (handle, exit) => + Effect.gen(function* () { + const successOptOut = Exit.isSuccess(exit) && options?.cleanupProcessGroupOnClose === false; + // `closeResult === undefined` is the honest liveness test: the pid is still owned, + // so signaling is safe and necessary; once the child has closed, signaling again + // would only race pid reuse. + if (successOptOut) { + yield* Effect.ignore(handle.unref); + } else if (closeResult === undefined) { + cleanedUpProcessGroup = true; + yield* signalGroup(handle, "SIGKILL"); + } + }), + ); + const pid = handle.pid; let stdout = ""; let stderr = ""; - let closeResult: RunResult | undefined; - let cleanedUpProcessGroup = false; - let disposedOwnHome = false; - const closeWaiters = new Set<(result: RunResult) => void>(); + let stdinError: unknown; + const closed = yield* Deferred.make(); + const stdoutChanged = yield* PubSub.unbounded(); - const cleanupProcessGroupOnClose = () => { + const cleanupProcessGroupOnClose = Effect.sync(() => { if (cleanedUpProcessGroup || !(options?.cleanupProcessGroupOnClose ?? true)) { return; } cleanedUpProcessGroup = true; - killProcessGroup(proc.pid!, "SIGKILL"); - }; + killProcessGroup(pid, "SIGKILL"); + }); - const disposeOwnHome = () => { - if (disposedOwnHome) { - return; - } - disposedOwnHome = true; - ownHome?.[Symbol.dispose](); - }; + const disposeOwnHome = ownHome === undefined ? Effect.void : disposeHome(ownHome); - stdoutStream.on("data", (data: Buffer) => { - stdout += data.toString(); - }); + const signal = (name: NodeJS.Signals) => signalGroup(handle, name); - stderrStream.on("data", (data: Buffer) => { - stderr += data.toString(); - }); + /* Each chunk is decoded on its own and keeps a byte order mark, like `Buffer#toString`. */ + const drain = ( + stream: Stream.Stream, + append: (chunk: string) => Effect.Effect, + ) => Stream.runForEach(stream, (chunk) => append(chunkDecoder.decode(chunk))).pipe(Effect.ignore); - proc.once("close", (code) => { - closeResult = { stdout, stderr, exitCode: code ?? 1 }; - for (const waiter of closeWaiters) { - waiter(closeResult); - } - closeWaiters.clear(); - }); + const stdoutDrained = yield* Effect.forkScoped( + drain(handle.stdout, (chunk) => + Effect.sync(() => { + stdout += chunk; + }).pipe(Effect.andThen(PubSub.publish(stdoutChanged, undefined))), + ), + ); + const stderrDrained = yield* Effect.forkScoped( + drain(handle.stderr, (chunk) => + Effect.sync(() => { + stderr += chunk; + }), + ), + ); + /* "close" in Node terms: the root exited and every holder of its stdout/stderr let go. */ + yield* Effect.forkScoped( + Effect.gen(function* () { + const [exitCode] = yield* Effect.all( + [ + /* A signal death has no code and reports exit code 1. */ + handle.exitCode.pipe( + Effect.map((code): number => code), + Effect.orElseSucceed(() => 1), + ), + Fiber.join(stdoutDrained), + Fiber.join(stderrDrained), + ], + { concurrency: "unbounded" }, + ); + const result: RunResult = { stdout, stderr, exitCode }; + closeResult = result; + yield* PubSub.publish(stdoutChanged, undefined); + yield* Deferred.succeed(closed, result); + }), + ); - let stdinError: unknown; - if (options?.stdin !== undefined && proc.stdin) { - proc.stdin.on("error", (error) => { - if (!("code" in error && error.code === "EPIPE")) { - stdinError = error; - } - }); - proc.stdin.write(options.stdin); - proc.stdin.end(); + if (options?.stdin !== undefined) { + yield* Effect.forkScoped( + Stream.run(Stream.make(new TextEncoder().encode(options.stdin)), handle.stdin).pipe( + Effect.catchTag("PlatformError", (error) => + Effect.sync(() => { + if (!isBrokenPipe(error)) stdinError = hostCause(error); + }), + ), + ), + ); } const stdinFailure = (result: RunResult) => @@ -531,7 +797,7 @@ export function spawnSupabase( [ `stdin write to the CLI failed`, `Command: supabase ${args.join(" ")}`, - `PID: ${proc.pid ?? ""}`, + `PID: ${pid}`, `exit code: ${result.exitCode}${ result.timedOutAfterMs === undefined ? "" @@ -543,172 +809,145 @@ export function spawnSupabase( { cause: stdinError }, ); - // The single exit path. `waitForExit` is the Promise facade over it, so both callers - // share one implementation of the exit bound, the process-group kill and home disposal. - const exitEffect = ( + const waitForExit = ( timeoutMs = options?.exitTimeoutMs ?? DEFAULT_EXIT_TIMEOUT_MS, - ): Effect.Effect => - Effect.callback((resume) => { - if (closeResult) { - cleanupProcessGroupOnClose(); - resume(Effect.succeed(closeResult)); - return Effect.void; - } - - let settled = false; - let timedOut = false; - - const timeout = setTimeout(() => { - timedOut = true; - killProcessGroup(proc.pid!, "SIGKILL"); - try { - proc.kill("SIGKILL"); - } catch {} - }, timeoutMs); - timeout.unref(); - - const onClose = (result: RunResult) => { - if (settled) return; - settled = true; - clearTimeout(timeout); - closeWaiters.delete(onClose); - cleanupProcessGroupOnClose(); - resume(Effect.succeed(timedOut ? { ...result, timedOutAfterMs: timeoutMs } : result)); - }; - - closeWaiters.add(onClose); - - return Effect.sync(() => { - settled = true; - clearTimeout(timeout); - closeWaiters.delete(onClose); - cleanupProcessGroupOnClose(); - // The home may still be in use when the opt-out kept the child alive here; in - // that case `releaseOwned` disposes it after the outer release kills the group. - if (closeResult !== undefined || cleanedUpProcessGroup) { - disposeOwnHome(); - } - }); - }).pipe( - // Disposal can throw; run it here so it fails the caller instead of escaping the - // `close` listener and leaving this effect pending. - Effect.tap(() => - Effect.try({ - try: disposeOwnHome, - catch: (cause) => new CliHomeDisposeError({ cause }), - }), + ): Effect.Effect => + Deferred.await(closed).pipe( + Effect.timeoutOrElse({ + duration: timeoutMs, + orElse: () => + signal("SIGKILL").pipe( + Effect.andThen(Deferred.await(closed)), + Effect.map((result): RunResult => ({ ...result, timedOutAfterMs: timeoutMs })), + ), + }), + Effect.tap(() => cleanupProcessGroupOnClose), + Effect.onInterrupt(() => + cleanupProcessGroupOnClose.pipe( + // The home may still be in use when the opt-out kept the child alive here; in + // that case the scope release disposes it. + Effect.andThen( + Effect.suspend(() => + closeResult !== undefined || cleanedUpProcessGroup + ? Effect.orDie(disposeOwnHome) + : Effect.void, + ), + ), + ), + ), + Effect.tap(() => disposeOwnHome), + Effect.flatMap((result) => + stdinError === undefined + ? Effect.succeed(result) + : Effect.fail(new CliStdinWriteError({ cause: stdinFailure(result) })), ), ); - const waitForExit = async ( - timeoutMs = options?.exitTimeoutMs ?? DEFAULT_EXIT_TIMEOUT_MS, - ): Promise => { - const result = await Effect.runPromise(exitEffect(timeoutMs)); - if (stdinError !== undefined) { - throw stdinFailure(result); - } - return result; + const outputWaitError = (reason: string, pattern: RegExp) => + new CliOutputWaitError({ + message: [ + `${reason} ${pattern}`, + `Command: supabase ${args.join(" ")}`, + `PID: ${pid}`, + outputTail("stdout tail", stdout), + outputTail("stderr tail", stderr), + ].join("\n\n"), + }); + + const outputMatches = (pattern: RegExp, startAt: number) => { + pattern.lastIndex = 0; + return pattern.test(stdout.slice(startAt)); }; - return { - pid: proc.pid!, + const waitForOutput = ( + pattern: RegExp, + timeoutMs = 60_000, + startAt = 0, + ): Effect.Effect => + Effect.scoped( + Effect.gen(function* () { + /* Subscribe before the first check so no chunk lands between the check and the wait. */ + const changes = yield* PubSub.subscribe(stdoutChanged); + while (!outputMatches(pattern, startAt)) { + if (closeResult !== undefined) { + return yield* outputWaitError("Process exited before output matched", pattern); + } + yield* PubSub.take(changes); + } + }), + ).pipe( + Effect.timeoutOrElse({ + duration: timeoutMs, + orElse: () => + Effect.fail(outputWaitError("Timed out waiting for output matching", pattern)), + }), + ); + + const spawned: SpawnedSupabase = { + pid, homeDir, stdout: () => stdout, stderr: () => stderr, - kill: (signal = "SIGTERM") => { - killProcessGroup(proc.pid!, signal); - try { - proc.kill(signal); - } catch {} - }, - waitForOutput: async (pattern: RegExp, timeoutMs = 60_000, startAt = 0) => { - pattern.lastIndex = 0; - if (pattern.test(stdout.slice(startAt))) { - return; - } - if (closeResult) { - throw new Error( - [ - `Process exited before output matched ${pattern}`, - `Command: supabase ${args.join(" ")}`, - `PID: ${proc.pid ?? ""}`, - outputTail("stdout tail", stdout), - outputTail("stderr tail", stderr), - ].join("\n\n"), - ); - } + kill: (name = "SIGTERM") => signal(name), + waitForOutput, + waitForExit, + }; + return spawned; +}); - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - cleanup(); - reject( - new Error( - [ - `Timed out waiting for output matching ${pattern}`, - `Command: supabase ${args.join(" ")}`, - `PID: ${proc.pid ?? ""}`, - outputTail("stdout tail", stdout), - outputTail("stderr tail", stderr), - ].join("\n\n"), - ), - ); - }, timeoutMs); - - const onStdout = (_data: Buffer) => { - pattern.lastIndex = 0; - if (pattern.test(stdout.slice(startAt))) { - cleanup(); - resolve(); - } - }; - - const onClose = () => { - cleanup(); - reject( - new Error( - [ - `Process exited before output matched ${pattern}`, - `Command: supabase ${args.join(" ")}`, - `PID: ${proc.pid ?? ""}`, - outputTail("stdout tail", stdout), - outputTail("stderr tail", stderr), - ].join("\n\n"), - ), - ); - }; +/** + * Spawns the compiled CLI in its own process group, owned by the calling scope: closing the + * scope kills a run that has not closed (unless `cleanupProcessGroupOnClose: false` and the + * scope succeeded) and disposes the run-owned temp home. + */ +export const spawnSupabase = ( + args: ReadonlyArray, + options?: SpawnSupabaseOptions, +): Effect.Effect => + spawnSupabaseProgram(args, options).pipe( + Effect.catchTags({ + PlatformError: (cause) => Effect.fail(new CliSpawnError({ cause: hostCause(cause) })), + CliBuildArtifactsError: (cause) => + Effect.fail(new CliSpawnError({ cause: hostOrPlainError(cause) })), + SchemaError: (cause) => Effect.fail(new CliSpawnError({ cause })), + }), + Effect.provide(helperServices), + ); - const cleanup = () => { - clearTimeout(timeout); - stdoutStream.off("data", onStdout); - proc.off("close", onClose); - }; +/** + * Effect-native CLI run. The spawned process group is owned by the run's scope, so an + * interrupted test kills the child instead of orphaning it; `cleanupProcessGroupOnClose: + * false` is honoured only on successful completion. `runSupabase` is the Promise facade + * over the same exit path. + */ +export const runSupabaseEffect = ( + args: string[], + options?: SpawnSupabaseOptions, +): Effect.Effect => + Effect.scoped(Effect.flatMap(spawnSupabase(args, options), (spawned) => spawned.waitForExit())); - stdoutStream.on("data", onStdout); - proc.on("close", onClose); - }); - }, - waitForExit, - exitEffect, - stdinFailure: (result) => (stdinError === undefined ? undefined : stdinFailure(result)), - releaseOwned: ({ successOptOut }) => { - // `closeResult === undefined` is the honest liveness test: the pid is still owned, - // so signaling is safe and necessary; once the child has closed, signaling again - // would only race pid reuse. The group flag alone cannot carry this — on Windows - // the group signal is a swallowed no-op, and the direct kill below is the only one - // that works there. - if (!successOptOut && closeResult === undefined) { - cleanedUpProcessGroup = true; - killProcessGroup(proc.pid!, "SIGKILL"); - try { - proc.kill("SIGKILL"); - } catch {} - } - disposeOwnHome(); - }, - }; -} +/** Kills the run once stdout matches `until`; the harness then reports exit code 0. */ +const runSupabaseUntil = ( + args: string[], + until: RegExp, + options: SpawnSupabaseOptions & { readonly untilTimeoutMs?: number }, +) => + Effect.scoped( + Effect.gen(function* () { + const spawned = yield* spawnSupabase(args, options); + yield* spawned.waitForOutput(until, options.untilTimeoutMs); + yield* spawned.kill("SIGTERM"); + yield* Effect.forkChild( + Effect.sleep(15_000).pipe( + Effect.andThen(Effect.sync(() => killProcessGroup(spawned.pid, "SIGKILL"))), + ), + ); + const result = yield* spawned.waitForExit(); + return { ...result, exitCode: 0 }; + }), + ); -export async function runSupabase( +export function runSupabase( args: string[], options?: { cwd?: string; @@ -726,62 +965,15 @@ export async function runSupabase( exitTimeoutMs?: number; }, ): Promise { - const spawned = spawnSupabase(args, options); - let killedByUntil = false; - - if (options?.until) { - await spawned.waitForOutput(options.until, options.untilTimeoutMs); - killedByUntil = true; - spawned.kill("SIGTERM"); - - const timer = setTimeout(() => { - killProcessGroup(spawned.pid, "SIGKILL"); - }, 15_000); - timer.unref(); - } - - const result = await spawned.waitForExit(); - return { ...result, exitCode: killedByUntil ? 0 : result.exitCode }; -} - -/** - * Effect-native CLI run. The spawned process group is owned by the calling scope, so an - * interrupted test kills the child instead of orphaning it; `cleanupProcessGroupOnClose: - * false` is honoured only on successful completion. `runSupabase` is the Promise facade - * over the same exit path. - */ -export const runSupabaseEffect = ( - args: string[], - options?: Parameters[1], -): Effect.Effect => - Effect.acquireRelease( - Effect.try({ - try: () => spawnSupabase(args, options), - catch: (cause) => new CliSpawnError({ cause }), - }), - // Scope teardown owns the spawned group: an interrupted run always kills it so the - // child is never orphaned, without re-signaling a group the close path already - // cleaned. On successful completion the caller's `cleanupProcessGroupOnClose: false` - // opt-out is honoured, matching the Promise path. - (spawned, exit) => - Effect.sync(() => - spawned.releaseOwned({ - successOptOut: Exit.isSuccess(exit) && options?.cleanupProcessGroupOnClose === false, - }), - ), - ).pipe( - Effect.flatMap((spawned) => - spawned.exitEffect().pipe( - Effect.flatMap((result) => { - const failure = spawned.stdinFailure(result); - return failure === undefined - ? Effect.succeed(result) - : Effect.fail(new CliStdinWriteError({ cause: failure })); - }), - ), + return Effect.runPromise( + Effect.mapError( + options?.until === undefined + ? runSupabaseEffect(args, options) + : runSupabaseUntil(args, options.until, options), + helperRejection, ), - Effect.scoped, ); +} export function requireCliSuccess( result: { diff --git a/apps/cli/tests/helpers/command-mocks.ts b/apps/cli/tests/helpers/command-mocks.ts index 313187abba..def4013944 100644 --- a/apps/cli/tests/helpers/command-mocks.ts +++ b/apps/cli/tests/helpers/command-mocks.ts @@ -1,18 +1,22 @@ import { legacyProjectStateLayer } from "../../src/command-internal/legacy-project-state.layer.ts"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { BunServices } from "@effect/platform-bun"; -import { type ApiClient, makeApiClient, type SupabaseApiConfigError } from "@supabase/api/effect"; +import { BunFileSystem, BunPath, BunServices } from "@effect/platform-bun"; +import { + type ApiClient, + makeApiClient, + type SupabaseApiConfigError, + type SupabaseApiError, +} from "@supabase/api/effect"; import { ConfigProvider, Effect, FileSystem, Layer, Option, + Path, Predicate, Redacted, + Schema, Sink, Stream, } from "effect"; @@ -77,6 +81,12 @@ import { processEnvLayer, } from "./mocks.ts"; +const hostPath = Effect.runSync(Path.Path.pipe(Effect.provide(BunPath.layer))); + +const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); +const encodeUnknownJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); +const encodeJson = (value: unknown) => encodeUnknownJson(value).pipe(Effect.orDie); + // Fixtures shared across every native-port integration test. Centralized so a change to the // project-ref schema (e.g. updated length) only needs to update one constant. export const VALID_REF = "abcdefghijklmnopqrst"; @@ -177,11 +187,9 @@ export function mockCommandCredentialsTracked( Effect.gen(function* () { deletedRefs.push(projectRef); if (opts.deleteFails === true) { - return yield* Effect.fail( - new CredentialDeleteError({ - message: "failed to delete project credential: permission denied", - }), - ); + return yield* new CredentialDeleteError({ + message: "failed to delete project credential: permission denied", + }); } return true; }), @@ -419,7 +427,7 @@ export function mockCommandSettings(opts: { githubToken: opts.githubToken ?? Option.none(), profile: opts.profile ?? "supabase", profileEnvValue: opts.profileEnvValue ?? Option.none(), - supabaseHome: opts.supabaseHome ?? join(opts.workdir, ".supabase"), + supabaseHome: opts.supabaseHome ?? hostPath.join(opts.workdir, ".supabase"), apiUrl: opts.apiUrl ?? DEFAULT_API_URL, projectHost: opts.projectHost ?? "supabase.co", poolerHost: opts.poolerHost ?? "supabase.com", @@ -553,11 +561,7 @@ export function mockCommandPlatformApi( let body: unknown = undefined; if (request.body._tag === "Uint8Array") { const decoded = new TextDecoder().decode(request.body.body); - try { - body = JSON.parse(decoded); - } catch { - body = decoded; - } + body = yield* decodeJson(decoded).pipe(Effect.orElseSucceed(() => decoded)); } const params = UrlParams.toString(request.urlParams); const recorded: RecordedRequest = { @@ -571,7 +575,7 @@ export function mockCommandPlatformApi( requests.push(recorded); if (opts.network === "fail") { - return yield* Effect.fail(transportFailure(request)); + return yield* transportFailure(request); } if (opts.handler !== undefined) { return yield* opts.handler(request, recorded); @@ -660,7 +664,7 @@ export function mockCommandPlatformApiService( Effect.gen(function* () { requests.push({ method: prop, input }); const stub = (stubs as Record)[prop] as - | ((i: unknown) => Effect.Effect) + | ((i: unknown) => Effect.Effect) | undefined; if (stub === undefined) { return yield* Effect.die(`Unmocked CommandPlatformApi.v1.${prop}`); @@ -691,7 +695,7 @@ export function mockCommandPlatformApiService( `https://api.supabase.com/mock-raw/${definition.id}`, ); if (stub === "fail") { - return yield* Effect.fail(transportFailure(request)); + return yield* transportFailure(request); } return jsonResponse(request, stub.status, stub.body); }); @@ -737,15 +741,30 @@ export function useTempWorkdir(prefix = "supabase-test-"): { readonly current: string; } { let root: string | undefined; - beforeEach(() => { - root = mkdtempSync(join(tmpdir(), prefix)); - }); - afterEach(() => { - if (root !== undefined) { - rmSync(root, { recursive: true, force: true }); - root = undefined; - } - }); + beforeEach(() => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + root = yield* fs.makeTempDirectory({ prefix }); + }).pipe( + /* A host failure rejects the hook with the raw host error. */ + Effect.catch((error) => Effect.die(error.cause ?? error)), + Effect.provide(BunFileSystem.layer), + Effect.runPromise, + ), + ); + afterEach(() => + Effect.gen(function* () { + if (root !== undefined) { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(root, { recursive: true, force: true }); + root = undefined; + } + }).pipe( + Effect.catch((error) => Effect.die(error.cause ?? error)), + Effect.provide(BunFileSystem.layer), + Effect.runPromise, + ), + ); return { get current() { if (root === undefined) { @@ -758,6 +777,14 @@ export function useTempWorkdir(prefix = "supabase-test-"): { }; } +/* Sets or unsets `name` in `env` and returns the value it replaced. */ +function setEnvVar(env: NodeJS.ProcessEnv, name: string, value: string | undefined) { + const previous = env[name]; + if (value === undefined) delete env[name]; + else env[name] = value; + return previous; +} + /** * Sets `name` to `value` (or unsets it when `value` is `undefined`) for the duration of `body`, * restoring whatever was there before. @@ -768,17 +795,11 @@ export const withEnvVar = ( body: Effect.Effect, ): Effect.Effect => Effect.acquireUseRelease( - Effect.sync(() => { - const previous = process.env[name]; - if (value === undefined) delete process.env[name]; - else process.env[name] = value; - return previous; - }), + Effect.sync(() => setEnvVar(process.env, name, value)), () => body, (previous) => Effect.sync(() => { - if (previous === undefined) delete process.env[name]; - else process.env[name] = previous; + setEnvVar(process.env, name, previous); }), ); @@ -980,15 +1001,13 @@ export function mockShadowContainerCliSpawner( const args = command._tag === "StandardCommand" ? command.args : []; spawned.push({ args }); if (command._tag !== "StandardCommand") { - return yield* Effect.fail( - new PlatformError( - new SystemError({ - _tag: "NotFound", - module: "ChildProcess", - method: "spawn", - description: "spawn failed", - }), - ), + return yield* new PlatformError( + new SystemError({ + _tag: "NotFound", + module: "ChildProcess", + method: "spawn", + description: "spawn failed", + }), ); } const isLocalDbInspect = @@ -1016,7 +1035,7 @@ export function mockShadowContainerCliSpawner( }); } if (isLocalDbInspect && opts.dbInspectImage !== undefined) { - const inspectJson = JSON.stringify([{ Config: { Image: opts.dbInspectImage } }]); + const inspectJson = yield* encodeJson([{ Config: { Image: opts.dbInspectImage } }]); return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(7000 + spawned.length), stdout: Stream.fromIterable([encoder.encode(inspectJson)]), @@ -1332,7 +1351,7 @@ export function mockDockerDaemonCliSpawner( stdout = container === undefined ? "" - : JSON.stringify({ + : yield* encodeJson({ Running: container.running, Status: container.running ? "running" : "exited", Health: { Status: "healthy" }, diff --git a/apps/cli/tests/helpers/docker-image.ts b/apps/cli/tests/helpers/docker-image.ts index cf388af81e..141dcd759a 100644 --- a/apps/cli/tests/helpers/docker-image.ts +++ b/apps/cli/tests/helpers/docker-image.ts @@ -1,5 +1,5 @@ import { BunServices } from "@effect/platform-bun"; -import { Cause, Duration, Effect, Layer } from "effect"; +import { Cause, Clock, Data, Duration, Effect, Layer } from "effect"; import * as ChildProcess from "effect/process/ChildProcess"; import { ChildProcessSpawner } from "effect/process"; import type { ChildProcessSpawner as ChildProcessSpawnerTag } from "effect/process/ChildProcessSpawner"; @@ -12,6 +12,10 @@ type Spawner = ChildProcessSpawnerTag["Service"]; // must leave the caller room to run its test body. export const RESOLVE_BUDGET_MS = 90_000; +class DockerImageError extends Data.TaggedError("DockerImageError")<{ + readonly message: string; +}> {} + const resolvedImages = new Map>(); /** @@ -57,8 +61,19 @@ export function ensureImage( // enqueued it — otherwise time spent waiting behind another image's resolution would // silently shrink this one's window. An explicit deadline is shared budget by contract and // is left as given. - return yield* resolveImage(spawner, image, deadline ?? resolveDeadline()); - }).pipe(Effect.provide(services), Effect.runPromise), + return yield* resolveImage( + spawner, + image, + deadline ?? (yield* deadlineAfter(RESOLVE_BUDGET_MS)), + ); + }).pipe( + /* The Promise rejects with a plain Error carrying the message. */ + Effect.catch((error) => + Effect.die(error instanceof DockerImageError ? new Error(error.message) : error), + ), + Effect.provide(services), + Effect.runPromise, + ), ); // Both the queue link and the memo swallow nothing: callers still see the // rejection, this only stops one failure from becoming an unhandled rejection @@ -76,7 +91,11 @@ export function ensureImage( * window. */ export function resolveDeadline(budgetMs = RESOLVE_BUDGET_MS): number { - return Date.now() + budgetMs; + return Effect.runSync(deadlineAfter(budgetMs)); +} + +function deadlineAfter(budgetMs: number): Effect.Effect { + return Effect.map(Clock.currentTimeMillis, (now) => now + budgetMs); } /** @@ -89,10 +108,16 @@ export function resolveDeadline(budgetMs = RESOLVE_BUDGET_MS): number { */ function requireDocker(spawner: Spawner): Effect.Effect { return spawner.exitCode(ChildProcess.make("docker", ["--version"])).pipe( - Effect.mapError(() => new Error("docker is required for this test but could not be spawned")), + Effect.mapError( + () => + new DockerImageError({ + message: "docker is required for this test but could not be spawned", + }), + ), Effect.filterOrFail( (exitCode) => Number(exitCode) === 0, - () => new Error("docker is required for this test but exited non-zero"), + () => + new DockerImageError({ message: "docker is required for this test but exited non-zero" }), ), Effect.asVoid, ); @@ -102,13 +127,17 @@ function requireDocker(spawner: Spawner): Effect.Effect { * The resolve itself, taking its spawner explicitly so unit tests can drive it * with a fake instead of a real Docker daemon. */ -export function resolveImage( +export const resolveImage: ( spawner: Spawner, image: string, deadline: number, -): Effect.Effect { - const remainingMs = Math.max(1, deadline - Date.now()); - return requireDocker(spawner).pipe( +) => Effect.Effect = Effect.fn("DockerImage.resolveImage")(function* ( + spawner: Spawner, + image: string, + deadline: number, +) { + const remainingMs = Math.max(1, deadline - (yield* Clock.currentTimeMillis)); + return yield* requireDocker(spawner).pipe( // The deadline goes into the resolver, which divides it across the registry candidates — a // stalled registry cannot starve the ECR → GHCR → Docker Hub fallbacks behind it, and an // exhausted share is reported against the candidate that spent it. The outer timeout is only @@ -119,9 +148,9 @@ export function resolveImage( Effect.timeout(Duration.millis(remainingMs + 1_000)), Effect.mapError((cause) => { if (Cause.isTimeoutError(cause)) { - return new Error( - `timed out resolving ${image} after ${remainingMs}ms — is the docker daemon responding?`, - ); + return new DockerImageError({ + message: `timed out resolving ${image} after ${remainingMs}ms — is the docker daemon responding?`, + }); } // The registry-pin hint only helps when the registries themselves were // the problem; gluing it onto a missing binary or an unreachable daemon @@ -129,7 +158,9 @@ export function resolveImage( const hint = cause.message.includes("failed to pull docker image from all registries") ? " (set SUPABASE_INTERNAL_IMAGE_REGISTRY to pin one)" : ""; - return new Error(`failed to resolve ${image}${hint}: ${cause.message}`); + return new DockerImageError({ + message: `failed to resolve ${image}${hint}: ${cause.message}`, + }); }), ); -} +}); diff --git a/apps/cli/tests/helpers/docker-image.unit.test.ts b/apps/cli/tests/helpers/docker-image.unit.test.ts index 50830a4cf3..89a885552e 100644 --- a/apps/cli/tests/helpers/docker-image.unit.test.ts +++ b/apps/cli/tests/helpers/docker-image.unit.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@effect/vitest"; -import { Deferred, Effect, Layer, PlatformError, Sink, Stream } from "effect"; +import { Clock, Deferred, Effect, Layer, PlatformError, Sink, Stream } from "effect"; import { ChildProcessSpawner } from "effect/process"; import { runDockerEffect } from "./cli.ts"; @@ -66,11 +66,13 @@ function mockSpawner( const IMAGE = "supabase/postgres:17"; describe("resolveDeadline", () => { - it("defaults to the shared budget and accepts a caller-sized one", () => { - const before = Date.now(); - expect(resolveDeadline()).toBeGreaterThanOrEqual(before + RESOLVE_BUDGET_MS - 50); - expect(resolveDeadline(1_000)).toBeLessThanOrEqual(Date.now() + 1_000); - }); + it.live("defaults to the shared budget and accepts a caller-sized one", () => + Effect.gen(function* () { + const before = yield* Clock.currentTimeMillis; + expect(resolveDeadline()).toBeGreaterThanOrEqual(before + RESOLVE_BUDGET_MS - 50); + expect(resolveDeadline(1_000)).toBeLessThanOrEqual((yield* Clock.currentTimeMillis) + 1_000); + }), + ); }); describe("resolveImage", () => { @@ -172,7 +174,8 @@ describe("resolveImage", () => { if (args[0] === "--version") return { exitCode: 0 }; return { exitCode: 1, stderr: "no such image" }; }); - return resolveImage(mock.spawner, IMAGE, Date.now() - 10_000).pipe( + return Clock.currentTimeMillis.pipe( + Effect.flatMap((now) => resolveImage(mock.spawner, IMAGE, now - 10_000)), Effect.flip, Effect.map((error) => { expect(error.message).toContain("candidate budget exhausted"); @@ -202,46 +205,64 @@ describe("ensureImage", () => { const layerFor = (mock: ReturnType) => Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, mock.spawner); - it("memoizes per image, including across differing deadlines", async () => { - const mock = mockSpawner(() => ({ exitCode: 0 })); - const image = `memo-${Date.now()}-a`; - const first = await ensureImage(image, resolveDeadline(5_000), layerFor(mock)); - const spawnsAfterFirst = mock.spawned.length; - const second = await ensureImage(image, resolveDeadline(60_000), layerFor(mock)); - expect(second).toBe(first); - expect(mock.spawned.length).toBe(spawnsAfterFirst); - }); + it.live("memoizes per image, including across differing deadlines", () => + Effect.gen(function* () { + const mock = mockSpawner(() => ({ exitCode: 0 })); + const image = `memo-${yield* Clock.currentTimeMillis}-a`; + const first = yield* Effect.promise(() => + ensureImage(image, resolveDeadline(5_000), layerFor(mock)), + ); + const spawnsAfterFirst = mock.spawned.length; + const second = yield* Effect.promise(() => + ensureImage(image, resolveDeadline(60_000), layerFor(mock)), + ); + expect(second).toBe(first); + expect(mock.spawned.length).toBe(spawnsAfterFirst); + }), + ); - it("memoizes failures so the retry ladder is never re-paid", async () => { - const mock = mockSpawner((args) => - args[0] === "--version" ? { exitCode: 0 } : { exitCode: 1, stderr: "no such image" }, - ); - const image = `memo-${Date.now()}-fail`; - await expect(ensureImage(image, Date.now() - 1_000, layerFor(mock))).rejects.toThrow(); - const spawnsAfterFirst = mock.spawned.length; - await expect(ensureImage(image, Date.now() - 1_000, layerFor(mock))).rejects.toThrow(); - expect(mock.spawned.length).toBe(spawnsAfterFirst); - }); + it.live("memoizes failures so the retry ladder is never re-paid", () => + Effect.gen(function* () { + const mock = mockSpawner((args) => + args[0] === "--version" ? { exitCode: 0 } : { exitCode: 1, stderr: "no such image" }, + ); + const image = `memo-${yield* Clock.currentTimeMillis}-fail`; + const firstDeadline = (yield* Clock.currentTimeMillis) - 1_000; + yield* Effect.promise(() => + expect(ensureImage(image, firstDeadline, layerFor(mock))).rejects.toThrow(), + ); + const spawnsAfterFirst = mock.spawned.length; + const secondDeadline = (yield* Clock.currentTimeMillis) - 1_000; + yield* Effect.promise(() => + expect(ensureImage(image, secondDeadline, layerFor(mock))).rejects.toThrow(), + ); + expect(mock.spawned.length).toBe(spawnsAfterFirst); + }), + ); - it("serializes distinct images: the second never spawns before the first settles", async () => { - const firstSpawnCounts: Array = []; - const mock = mockSpawner((args) => { - if (args[0] === "--version") return { exitCode: 0 }; - return { exitCode: 1, stderr: "no such image" }; - }); - const imageA = `queue-${Date.now()}-a`; - const imageB = `queue-${Date.now()}-b`; - // Enqueue both before awaiting either; the queue must fully settle A - // (including its failure) before B's first spawn happens. - const a = ensureImage(imageA, Date.now() - 1_000, layerFor(mock)).catch(() => "a-done"); - const spawnsWhenBEnqueued = mock.spawned.length; - const b = ensureImage(imageB, Date.now() - 1_000, layerFor(mock)).catch(() => "b-done"); - expect(mock.spawned.length).toBe(spawnsWhenBEnqueued); - await a; - firstSpawnCounts.push(mock.spawned.length); - await b; - expect(firstSpawnCounts[0]).toBeLessThanOrEqual(mock.spawned.length); - }); + it.live("serializes distinct images: the second never spawns before the first settles", () => + Effect.gen(function* () { + const firstSpawnCounts: Array = []; + const mock = mockSpawner((args) => { + if (args[0] === "--version") return { exitCode: 0 }; + return { exitCode: 1, stderr: "no such image" }; + }); + const imageA = `queue-${yield* Clock.currentTimeMillis}-a`; + const imageB = `queue-${yield* Clock.currentTimeMillis}-b`; + const deadlineA = (yield* Clock.currentTimeMillis) - 1_000; + const deadlineB = (yield* Clock.currentTimeMillis) - 1_000; + // Enqueue both before awaiting either; the queue must fully settle A + // (including its failure) before B's first spawn happens. + const a = ensureImage(imageA, deadlineA, layerFor(mock)).catch(() => "a-done"); + const spawnsWhenBEnqueued = mock.spawned.length; + const b = ensureImage(imageB, deadlineB, layerFor(mock)).catch(() => "b-done"); + expect(mock.spawned.length).toBe(spawnsWhenBEnqueued); + yield* Effect.promise(() => a); + firstSpawnCounts.push(mock.spawned.length); + yield* Effect.promise(() => b); + expect(firstSpawnCounts[0]).toBeLessThanOrEqual(mock.spawned.length); + }), + ); }); describe("runDockerEffect", () => { diff --git a/apps/cli/tests/helpers/legacy-state-guard.ts b/apps/cli/tests/helpers/legacy-state-guard.ts index f023936a5b..b7f702601d 100644 --- a/apps/cli/tests/helpers/legacy-state-guard.ts +++ b/apps/cli/tests/helpers/legacy-state-guard.ts @@ -1,10 +1,12 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { BunPath, BunServices } from "@effect/platform-bun"; +import { Effect, FileSystem, Layer, Path } from "effect"; -import { BunServices } from "@effect/platform-bun"; -import { Effect, FileSystem, Layer } from "effect"; - -const LEGACY_STATE_SEGMENT = join("supabase", ".temp"); +const LEGACY_STATE_SEGMENT = Effect.runSync( + Effect.gen(function* () { + const path = yield* Path.Path; + return path.join("supabase", ".temp"); + }).pipe(Effect.provide(BunPath.layer)), +); /** Wrong-on-purpose values for every `supabase/.temp` file a stack-mode command might still read. */ const LEGACY_STATE_DECOYS: Readonly> = { @@ -27,13 +29,20 @@ const LEGACY_STATE_DECOYS: Readonly> = { }; /** Writes the decoys into `/supabase/.temp`, outside any recording filesystem. */ -export function seedLegacyStateDecoys(root: string): void { - const dir = join(root, LEGACY_STATE_SEGMENT); - mkdirSync(dir, { recursive: true }); - for (const [name, value] of Object.entries(LEGACY_STATE_DECOYS)) { - writeFileSync(join(dir, name), value); - } -} +export const seedLegacyStateDecoys = (root: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = path.join(root, LEGACY_STATE_SEGMENT); + yield* fs.makeDirectory(dir, { recursive: true }); + for (const [name, value] of Object.entries(LEGACY_STATE_DECOYS)) { + yield* fs.writeFileString(path.join(dir, name), value); + } + }).pipe( + /* A host failure surfaces as the raw host error. */ + Effect.catchTag("PlatformError", (error) => Effect.die(error.cause ?? error)), + Effect.provide(BunServices.layer), + ); /** The recorded paths that fall under a `supabase/.temp` directory. */ export function legacyStateAccesses(accessed: ReadonlyArray): ReadonlyArray { diff --git a/apps/cli/tests/helpers/live-env.ts b/apps/cli/tests/helpers/live-env.ts index 284e40906b..fb154931a1 100644 --- a/apps/cli/tests/helpers/live-env.ts +++ b/apps/cli/tests/helpers/live-env.ts @@ -1,5 +1,7 @@ /** Environment-only live-suite configuration. */ +import { Config, Data, Effect, Option } from "effect"; + export const LIVE_EXIT_TIMEOUT_MS = 240_000; /** @@ -8,51 +10,77 @@ export const LIVE_EXIT_TIMEOUT_MS = 240_000; */ export const LIVE_SHADOW_PORT = "24320"; -export function liveApiUrl(): string { - const value = process.env["SUPABASE_LIVE_API_URL"]?.trim(); +class LiveConfigError extends Data.TaggedError("LiveConfigError")<{ + readonly message: string; +}> {} + +const envValue = (name: string) => + Config.option(Config.String(name)).pipe( + Effect.map(Option.getOrUndefined), + Effect.mapError((error) => new LiveConfigError({ message: error.message })), + ); + +export const liveApiUrl = Effect.gen(function* () { + const value = (yield* envValue("SUPABASE_LIVE_API_URL"))?.trim(); if (value === undefined || value.length === 0) { - throw new Error("SUPABASE_LIVE_API_URL is required to run the live suite"); - } - let url: URL; - try { - url = new URL(value); - } catch { - throw new Error(`SUPABASE_LIVE_API_URL must be an absolute HTTP(S) URL: ${value}`); + return yield* new LiveConfigError({ + message: "SUPABASE_LIVE_API_URL is required to run the live suite", + }); } + const url = yield* Effect.try({ + try: () => new URL(value), + catch: () => + new LiveConfigError({ + message: `SUPABASE_LIVE_API_URL must be an absolute HTTP(S) URL: ${value}`, + }), + }); if (url.protocol !== "http:" && url.protocol !== "https:") { - throw new Error(`SUPABASE_LIVE_API_URL must use http:// or https://: ${value}`); + return yield* new LiveConfigError({ + message: `SUPABASE_LIVE_API_URL must use http:// or https://: ${value}`, + }); } return url.toString().replace(/\/+$/u, ""); -} +}); -export function liveAccessToken(): string { - const token = process.env["SUPABASE_ACCESS_TOKEN"]?.trim(); +export const liveAccessToken = Effect.gen(function* () { + const token = (yield* envValue("SUPABASE_ACCESS_TOKEN"))?.trim(); if (token === undefined || token.length === 0) { - throw new Error("SUPABASE_ACCESS_TOKEN is required to run the live suite"); + return yield* new LiveConfigError({ + message: "SUPABASE_ACCESS_TOKEN is required to run the live suite", + }); } return token; -} +}); -export function validateLiveConfig(): { readonly apiUrl: string; readonly accessToken: string } { - return { apiUrl: liveApiUrl(), accessToken: liveAccessToken() }; -} +export const validateLiveConfig = Effect.gen(function* () { + const apiUrl = yield* liveApiUrl; + const accessToken = yield* liveAccessToken; + return { apiUrl, accessToken }; +}); -export function keepLiveProject(): boolean { - return process.env["SUPABASE_LIVE_KEEP_PROJECT"] === "1"; -} +export const keepLiveProject = Effect.map( + envValue("SUPABASE_LIVE_KEEP_PROJECT"), + (value) => value === "1", +); -export function liveProjectName(): string { - return process.env["SUPABASE_LIVE_PROJECT_NAME"]?.trim() || "supabase-cli-live"; -} +export const liveProjectName = Effect.map( + envValue("SUPABASE_LIVE_PROJECT_NAME"), + (value) => value?.trim() || "supabase-cli-live", +); -export function liveRegion(): string { - return process.env["SUPABASE_LIVE_REGION"]?.trim() || "us-east-1"; -} +export const liveRegion = Effect.map( + envValue("SUPABASE_LIVE_REGION"), + (value) => value?.trim() || "us-east-1", +); -export function liveOrgId(): string | undefined { - const value = process.env["SUPABASE_LIVE_ORG_ID"]?.trim(); - return value === undefined || value.length === 0 ? undefined : value; -} +export const liveRunId = Effect.gen(function* () { + return (yield* envValue("GITHUB_RUN_ID")) ?? (yield* envValue("CI_JOB_ID")); +}); + +export const liveOrgId = Effect.map(envValue("SUPABASE_LIVE_ORG_ID"), (value) => { + const trimmed = value?.trim(); + return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed; +}); /** Resolve `.` from a database host such as `db..supabase.co`. */ export function deriveLiveProjectHost(databaseHost: string, projectRef: string): string { diff --git a/apps/cli/tests/helpers/live-env.unit.test.ts b/apps/cli/tests/helpers/live-env.unit.test.ts index b5704a6a9c..1383862cfe 100644 --- a/apps/cli/tests/helpers/live-env.unit.test.ts +++ b/apps/cli/tests/helpers/live-env.unit.test.ts @@ -1,36 +1,39 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { ConfigProvider, Effect } from "effect"; +import { describe, expect, it } from "vitest"; import { deriveLiveProjectHost, liveApiUrl, validateLiveConfig } from "./live-env.ts"; -const originalApiUrl = process.env["SUPABASE_LIVE_API_URL"]; -const originalToken = process.env["SUPABASE_ACCESS_TOKEN"]; - -afterEach(() => { - if (originalApiUrl === undefined) delete process.env["SUPABASE_LIVE_API_URL"]; - else process.env["SUPABASE_LIVE_API_URL"] = originalApiUrl; - if (originalToken === undefined) delete process.env["SUPABASE_ACCESS_TOKEN"]; - else process.env["SUPABASE_ACCESS_TOKEN"] = originalToken; -}); +const runWithEnv = (env: Record, effect: Effect.Effect): A => + Effect.runSync( + Effect.provideService( + effect, + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnvRecord(env, { preserveEmptyStrings: true }), + ), + ); describe("live environment", () => { it("requires both the API URL and access token", () => { - delete process.env["SUPABASE_LIVE_API_URL"]; - delete process.env["SUPABASE_ACCESS_TOKEN"]; - expect(() => validateLiveConfig()).toThrow("SUPABASE_LIVE_API_URL is required"); + expect(() => runWithEnv({}, validateLiveConfig)).toThrow("SUPABASE_LIVE_API_URL is required"); - process.env["SUPABASE_LIVE_API_URL"] = "http://localhost:8080"; - expect(() => validateLiveConfig()).toThrow("SUPABASE_ACCESS_TOKEN is required"); + const apiUrlOnly = { SUPABASE_LIVE_API_URL: "http://localhost:8080" }; + expect(() => runWithEnv(apiUrlOnly, validateLiveConfig)).toThrow( + "SUPABASE_ACCESS_TOKEN is required", + ); }); it("normalizes and validates HTTP API URLs", () => { - process.env["SUPABASE_LIVE_API_URL"] = "http://localhost:8080///"; - process.env["SUPABASE_ACCESS_TOKEN"] = " token "; - expect(validateLiveConfig()).toEqual({ + const env = { + SUPABASE_LIVE_API_URL: "http://localhost:8080///", + SUPABASE_ACCESS_TOKEN: " token ", + }; + expect(runWithEnv(env, validateLiveConfig)).toEqual({ apiUrl: "http://localhost:8080", accessToken: "token", }); - process.env["SUPABASE_LIVE_API_URL"] = "not-a-url"; - expect(() => liveApiUrl()).toThrow("absolute HTTP(S) URL"); + expect(() => runWithEnv({ ...env, SUPABASE_LIVE_API_URL: "not-a-url" }, liveApiUrl)).toThrow( + "absolute HTTP(S) URL", + ); }); it("derives the project host from the typed database host", () => { diff --git a/apps/cli/tests/helpers/live-project.integration.test.ts b/apps/cli/tests/helpers/live-project.integration.test.ts index 3472036075..3d99a9c9fe 100644 --- a/apps/cli/tests/helpers/live-project.integration.test.ts +++ b/apps/cli/tests/helpers/live-project.integration.test.ts @@ -1,10 +1,12 @@ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { makeApiClient } from "@supabase/api/effect"; -import { Effect, Layer } from "effect"; +import { Cause, Effect, Exit, Layer } from "effect"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientRequest from "effect/http/HttpClientRequest"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { cliConfigProviderLayer } from "../../src/shared/config/cli-config-provider.layer.ts"; import { provisionLiveEnvironment } from "./live-project.ts"; const ref = "abcdefghijklmnopqrst"; @@ -65,7 +67,7 @@ describe("live project provisioning", () => { body: "sentinel-secret", headers: {}, }, - ])("redacts $name while cleaning up", async ({ status, body, headers }) => { + ])("redacts $name while cleaning up", ({ status, body, headers }) => { vi.stubEnv("SUPABASE_LIVE_API_URL", "https://api.supabase.green"); vi.stubEnv("SUPABASE_ACCESS_TOKEN", "test-token"); vi.stubEnv("SUPABASE_LIVE_ORG_ID", "org-slug"); @@ -74,66 +76,65 @@ describe("live project provisioning", () => { const requests: string[] = []; const ownedProjects = new Set(); - const client = await Effect.runPromise( - makeApiClient({ baseUrl: "https://api.supabase.green", accessToken: "test-token" }).pipe( + const platformApi = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => { + const url = new URL(request.url); + requests.push(`${request.method} ${url.pathname}`); + if (request.method === "GET" && url.pathname === "/v1/organizations") { + return Effect.succeed( + jsonResponse(request, 200, [{ id: "org-id", slug: "org-slug", name: "Test" }]), + ); + } + if (request.method === "POST" && url.pathname === "/v1/projects") { + ownedProjects.add(ref); + return Effect.succeed(jsonResponse(request, 201, { ...project, database: undefined })); + } + if (request.method === "GET" && url.pathname === `/v1/projects/${ref}`) { + return Effect.succeed(jsonResponse(request, 200, project)); + } + if (request.method === "GET" && url.pathname === `/v1/projects/${ref}/api-keys`) { + return Effect.succeed(jsonResponse(request, status, body, headers)); + } + if (request.method === "DELETE" && url.pathname === `/v1/projects/${ref}`) { + ownedProjects.delete(ref); + return Effect.succeed(jsonResponse(request, 200, { id: 1, ref, name: "live-test" })); + } + return Effect.die(`unexpected request ${request.method} ${url.pathname}`); + }), + ); + + return Effect.runPromise( + Effect.gen(function* () { + const client = yield* makeApiClient({ + baseUrl: "https://api.supabase.green", + accessToken: "test-token", + }); + + const exit = yield* Effect.exit(provisionLiveEnvironment(client)); + const failure = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined; + if (status === 403) { + expect(String(failure)).toMatch( + /project API keys failed: GET \/v1\/projects\/abcdefghijklmnopqrst\/api-keys returned HTTP 403.*x-request-id=req-live-403.*response body omitted/, + ); + } else { + expect(String(failure)).toContain( + "project API keys failed: management API response schema validation failed", + ); + } + expect(requests.filter((request) => request.includes("/api-keys"))).toHaveLength(1); + expect(requests).toContain("DELETE /v1/projects/abcdefghijklmnopqrst"); + expect(ownedProjects).toEqual(new Set()); + const message = String(failure); + expect(message).not.toContain("test-token"); + expect(message).not.toContain("secret-value"); + expect(message).not.toContain("authorization denied"); + expect(message).not.toContain("sentinel-secret"); + }).pipe( Effect.provide( - Layer.succeed( - HttpClient.HttpClient, - HttpClient.make((request) => { - const url = new URL(request.url); - requests.push(`${request.method} ${url.pathname}`); - if (request.method === "GET" && url.pathname === "/v1/organizations") { - return Effect.succeed( - jsonResponse(request, 200, [{ id: "org-id", slug: "org-slug", name: "Test" }]), - ); - } - if (request.method === "POST" && url.pathname === "/v1/projects") { - ownedProjects.add(ref); - return Effect.succeed( - jsonResponse(request, 201, { ...project, database: undefined }), - ); - } - if (request.method === "GET" && url.pathname === `/v1/projects/${ref}`) { - return Effect.succeed(jsonResponse(request, 200, project)); - } - if (request.method === "GET" && url.pathname === `/v1/projects/${ref}/api-keys`) { - return Effect.succeed(jsonResponse(request, status, body, headers)); - } - if (request.method === "DELETE" && url.pathname === `/v1/projects/${ref}`) { - ownedProjects.delete(ref); - return Effect.succeed( - jsonResponse(request, 200, { id: 1, ref, name: "live-test" }), - ); - } - return Effect.die(`unexpected request ${request.method} ${url.pathname}`); - }), - ), + Layer.mergeAll(platformApi, BunFileSystem.layer, BunPath.layer, cliConfigProviderLayer), ), ), ); - - let failure: unknown; - try { - await Effect.runPromise(provisionLiveEnvironment(client)); - } catch (error) { - failure = error; - } - if (status === 403) { - expect(String(failure)).toMatch( - /project API keys failed: GET \/v1\/projects\/abcdefghijklmnopqrst\/api-keys returned HTTP 403.*x-request-id=req-live-403.*response body omitted/, - ); - } else { - expect(String(failure)).toContain( - "project API keys failed: management API response schema validation failed", - ); - } - expect(requests.filter((request) => request.includes("/api-keys"))).toHaveLength(1); - expect(requests).toContain("DELETE /v1/projects/abcdefghijklmnopqrst"); - expect(ownedProjects).toEqual(new Set()); - const message = String(failure); - expect(message).not.toContain("test-token"); - expect(message).not.toContain("secret-value"); - expect(message).not.toContain("authorization denied"); - expect(message).not.toContain("sentinel-secret"); }); }); diff --git a/apps/cli/tests/helpers/live-project.ts b/apps/cli/tests/helpers/live-project.ts index 70765ff971..f63727ec70 100644 --- a/apps/cli/tests/helpers/live-project.ts +++ b/apps/cli/tests/helpers/live-project.ts @@ -1,11 +1,22 @@ import { randomBytes, randomUUID } from "node:crypto"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; import { makeApiClient, type OperationOutput } from "@supabase/api/effect"; -import { Cause, Data, Effect, Exit, Schedule, Schema } from "effect"; +import { + Cause, + Clock, + Console, + Data, + Effect, + Exit, + FileSystem, + Path, + type PlatformError, + Schedule, + Schema, +} from "effect"; +import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientError from "effect/http/HttpClientError"; +import * as HttpClientRequest from "effect/http/HttpClientRequest"; import { getDomain } from "tldts"; import { @@ -15,6 +26,7 @@ import { liveOrgId, liveProjectName, liveRegion, + liveRunId, } from "./live-env.ts"; const PROJECT_REF_RE = /^[a-z]{20}$/u; @@ -94,10 +106,19 @@ class LiveStorageError extends Data.TaggedError("LiveStorageError")<{ readonly cause?: unknown; }> {} +class LiveProjectError extends Data.TaggedError("LiveProjectError")<{ + readonly message: string; +}> {} + function apiError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)); } +/** Filesystem failures surface as the underlying Node error. */ +function platformCause(error: PlatformError.PlatformError): Error { + return apiError(error.cause); +} + const DIAGNOSTIC_HEADER_NAMES = [ "x-request-id", "x-correlation-id", @@ -140,7 +161,11 @@ function liveApiErrorMessage(phase: string, cause: unknown): string { export function supportedRegion(value: string): Effect.Effect { const region = REGIONS.find((candidate) => candidate === value); return region === undefined - ? Effect.fail(new Error(`Unsupported SUPABASE_LIVE_REGION ${JSON.stringify(value)}`)) + ? Effect.fail( + new LiveProjectError({ + message: `Unsupported SUPABASE_LIVE_REGION ${JSON.stringify(value)}`, + }), + ) : Effect.succeed(region); } @@ -229,14 +254,14 @@ function timeoutLiveRequest( ): Effect.Effect { return Effect.timeoutOrElse(effect, { duration: POLL_TIMEOUT, - orElse: () => Effect.fail(new Error(`${phase} timed out`)), + orElse: () => Effect.fail(new LiveProjectError({ message: `${phase} timed out` })), }); } -function uniqueProjectName(): string { - const runId = process.env["GITHUB_RUN_ID"] ?? process.env["CI_JOB_ID"] ?? String(Date.now()); - return `${liveProjectName()}-${runId}-${randomUUID().slice(0, 8)}`; -} +const uniqueProjectName = Effect.gen(function* () { + const runId = (yield* liveRunId) ?? String(yield* Clock.currentTimeMillis); + return `${yield* liveProjectName}-${runId}-${randomUUID().slice(0, 8)}`; +}); function databasePassword(): string { return `supabase-cli-live-${randomBytes(12).toString("hex")}`; @@ -245,24 +270,26 @@ function databasePassword(): string { function resolveOrganization(api: LiveApi): Effect.Effect { return timeoutLiveRequest("organization lookup", api.v1.listAllOrganizations()).pipe( Effect.mapError(apiError), - Effect.flatMap((organizations) => { - const requested = liveOrgId(); - const organization = - requested === undefined - ? organizations[0] - : organizations.find( - (candidate) => candidate.id === requested || candidate.slug === requested, - ); - return organization === undefined - ? Effect.fail( - new Error( - requested === undefined - ? "No organizations found; cannot create the live project" - : `Organization ${requested} was not found; cannot create the live project`, - ), - ) - : Effect.succeed(organization); - }), + Effect.flatMap((organizations) => + Effect.flatMap(liveOrgId, (requested) => { + const organization = + requested === undefined + ? organizations[0] + : organizations.find( + (candidate) => candidate.id === requested || candidate.slug === requested, + ); + return organization === undefined + ? Effect.fail( + new LiveProjectError({ + message: + requested === undefined + ? "No organizations found; cannot create the live project" + : `Organization ${requested} was not found; cannot create the live project`, + }), + ) + : Effect.succeed(organization); + }), + ), ); } @@ -271,7 +298,8 @@ function createProject( name: string, password: string, ): Effect.Effect { - return supportedRegion(liveRegion()).pipe( + return liveRegion.pipe( + Effect.flatMap(supportedRegion), Effect.flatMap((region) => resolveOrganization(api).pipe( Effect.flatMap((organization) => @@ -290,7 +318,11 @@ function createProject( Effect.flatMap((project) => PROJECT_REF_RE.test(project.ref) ? Effect.succeed(project.ref) - : Effect.fail(new Error(`Unexpected project ref from project creation: ${project.ref}`)), + : Effect.fail( + new LiveProjectError({ + message: `Unexpected project ref from project creation: ${project.ref}`, + }), + ), ), ); } @@ -335,8 +367,9 @@ function waitForProject(api: LiveApi, ref: string): Effect.Effect error instanceof LiveTransientPoll, }).pipe( Effect.mapError((error) => { - if (error instanceof LiveTerminalPoll) return new Error(error.message); - if (error instanceof LivePollTimeout) return new Error(error.message); + if (error instanceof LiveTerminalPoll) + return new LiveProjectError({ message: error.message }); + if (error instanceof LivePollTimeout) return new LiveProjectError({ message: error.message }); return apiError(error); }), ); @@ -378,9 +411,11 @@ function resolveKeys( }).pipe( Effect.mapError((error) => error instanceof LivePollTimeout - ? new Error(`Project ${ref} did not return API keys within ${POLL_TIMEOUT}`) + ? new LiveProjectError({ + message: `Project ${ref} did not return API keys within ${POLL_TIMEOUT}`, + }) : error instanceof LiveTerminalPoll - ? new Error(error.message) + ? new LiveProjectError({ message: error.message }) : apiError(error), ), ); @@ -437,11 +472,11 @@ function resolveDbUrl( }).pipe( Effect.mapError((error) => error instanceof LivePollTimeout - ? new Error( - `Project ${ref} did not return a pooler connection string within ${POLL_TIMEOUT}`, - ) + ? new LiveProjectError({ + message: `Project ${ref} did not return a pooler connection string within ${POLL_TIMEOUT}`, + }) : error instanceof LiveTerminalPoll - ? new Error(error.message) + ? new LiveProjectError({ message: error.message }) : apiError(error), ), ); @@ -452,85 +487,111 @@ function createStorageBucket( host: string, serviceRoleKey: string, bucket: string, -): Effect.Effect { - const attempt = Effect.tryPromise({ - try: async (signal) => { - const response = await fetch(`https://${ref}.${host}/storage/v1/bucket`, { - method: "POST", - headers: { Authorization: `Bearer ${serviceRoleKey}`, "Content-Type": "application/json" }, - body: JSON.stringify({ id: bucket, name: bucket, public: false }), - signal, - }); - if (!response.ok && response.status !== 409) { - throw new LiveStorageError({ - message: `Failed to create storage bucket ${bucket}: ${response.status} ${await response.text()}`, +): Effect.Effect { + return Effect.gen(function* () { + const client = yield* HttpClient.HttpClient; + const attempt = Effect.gen(function* () { + const request = yield* HttpClientRequest.post( + `https://${ref}.${host}/storage/v1/bucket`, + ).pipe( + HttpClientRequest.setHeaders({ + Authorization: `Bearer ${serviceRoleKey}`, + "Content-Type": "application/json", + }), + HttpClientRequest.bodyJson({ id: bucket, name: bucket, public: false }), + ); + const response = yield* client.execute(request); + const ok = response.status >= 200 && response.status <= 299; + if (!ok && response.status !== 409) { + return yield* new LiveStorageError({ + message: `Failed to create storage bucket ${bucket}: ${response.status} ${yield* response.text}`, retryable: isTransientStorageStatus(response.status), }); } - }, - catch: (cause) => - cause instanceof LiveStorageError - ? cause - : new LiveStorageError({ - message: `Failed to create storage bucket ${bucket}: ${apiError(cause).message}`, - retryable: true, - cause, - }), + }).pipe( + Effect.mapError((cause) => + cause instanceof LiveStorageError + ? cause + : new LiveStorageError({ + message: `Failed to create storage bucket ${bucket}: ${apiError(cause).message}`, + retryable: true, + cause, + }), + ), + Effect.provideService(HttpClient.TracerPropagationEnabled, false), + ); + return yield* retryLiveEffect("storage bucket", attempt, { + shouldRetry: (error) => error instanceof LiveStorageError && error.retryable, + }).pipe( + Effect.mapError((error) => + error instanceof LivePollTimeout + ? new LiveProjectError({ message: `storage bucket ${bucket} creation timed out` }) + : error instanceof LiveStorageError + ? new LiveProjectError({ message: error.message }) + : apiError(error), + ), + ); }); - return retryLiveEffect("storage bucket", attempt, { - shouldRetry: (error) => error instanceof LiveStorageError && error.retryable, - }).pipe( - Effect.mapError((error) => - error instanceof LivePollTimeout - ? new Error(`storage bucket ${bucket} creation timed out`) - : error instanceof LiveStorageError - ? new Error(error.message) - : apiError(error), - ), - ); } function writeProfile( projectRef: string, projectHost: string, dbUrl: string, -): Effect.Effect { - return Effect.tryPromise({ - try: async () => { - const directory = await mkdtemp(path.join(tmpdir(), "supabase-live-profile-")); - const profilePath = path.join(directory, "profile.yaml"); - try { - const poolerHost = resolvePoolerDomain(dbUrl); - await writeFile( +): Effect.Effect { + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const directory = yield* fs + .makeTempDirectory({ prefix: "supabase-live-profile-" }) + .pipe(Effect.mapError(platformCause)); + const profilePath = path.join(directory, "profile.yaml"); + const write = Effect.gen(function* () { + const poolerHost = yield* Effect.try({ + try: () => resolvePoolerDomain(dbUrl), + catch: (cause) => new LiveProjectError({ message: apiError(cause).message }), + }); + const apiUrl = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.String))( + yield* liveApiUrl, + ); + yield* fs + .writeFileString( profilePath, [ `name: ${PROFILE_NAME}`, - `api_url: ${JSON.stringify(liveApiUrl())}`, - `dashboard_url: ${JSON.stringify(liveApiUrl())}`, + `api_url: ${apiUrl}`, + `dashboard_url: ${apiUrl}`, `project_host: ${projectHost}`, `pooler_host: ${poolerHost}`, `# provisioned project: ${projectRef}`, "", ].join("\n"), - ); - return profilePath; - } catch (cause) { - try { - await rm(directory, { recursive: true, force: true }); - } catch (cleanup) { - throw cleanupErrors(cause, [cleanup]); - } - throw cause; - } - }, - catch: apiError, + ) + .pipe(Effect.mapError(platformCause)); + return profilePath; + }); + return yield* write.pipe( + Effect.catch((cause) => + fs.remove(directory, { recursive: true, force: true }).pipe( + Effect.matchEffect({ + onFailure: (cleanup) => Effect.fail(cleanupErrors(cause, [platformCause(cleanup)])), + onSuccess: () => Effect.fail(cause), + }), + ), + ), + ); }); } -function cleanupDirectory(profilePath: string): Effect.Effect { - return Effect.tryPromise({ - try: () => rm(path.dirname(profilePath), { recursive: true, force: true }), - catch: apiError, +function cleanupDirectory( + profilePath: string, +): Effect.Effect { + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs + .remove(path.dirname(profilePath), { recursive: true, force: true }) + .pipe(Effect.mapError(platformCause)); }); } @@ -538,21 +599,21 @@ function cleanupRemote( api: LiveApi, environment: LiveCliProjectEnvironment, ): Effect.Effect { - return keepLiveProject() - ? Effect.sync(() => { - console.log(`SUPABASE_LIVE_KEEP_PROJECT=1 — leaving ${environment.project.ref} alive`); - }) - : deleteProject(api, environment.project.ref); + return Effect.flatMap(keepLiveProject, (keep) => + keep + ? Console.log(`SUPABASE_LIVE_KEEP_PROJECT=1 — leaving ${environment.project.ref} alive`) + : deleteProject(api, environment.project.ref), + ); } function cleanupCreatedProject(api: LiveApi, ref: string): Effect.Effect { - return keepLiveProject() - ? Effect.sync(() => { - console.log( + return Effect.flatMap(keepLiveProject, (keep) => + keep + ? Console.log( `SUPABASE_LIVE_KEEP_PROJECT=1 — leaving ${ref} alive after provisioning failure`, - ); - }) - : deleteProject(api, ref); + ) + : deleteProject(api, ref), + ); } function combineCleanupExits( @@ -578,57 +639,59 @@ export interface LiveCliProjectEnvironment { } /** Provision one project; the caller owns the outer Effect runtime boundary. */ -export function provisionLiveEnvironment( +export const provisionLiveEnvironment: ( api: LiveApi, -): Effect.Effect { - return Effect.gen(function* () { - const password = databasePassword(); - const ref = yield* createProject(api, uniqueProjectName(), password); - const setup = Effect.gen(function* () { - const project = yield* waitForProject(api, ref); - const projectHost = deriveLiveProjectHost(project.database.host, ref); - const keys = yield* resolveKeys(api, ref); - const dbUrl = yield* resolveDbUrl(api, ref, password); - const storageBucket = "supabase-cli-live-bucket"; - yield* createStorageBucket(ref, projectHost, keys.serviceRoleKey, storageBucket); - const profilePath = yield* writeProfile(ref, projectHost, dbUrl); - return { - project: { - ref, - dbUrl, - dbPassword: password, - anonKey: keys.anonKey, - serviceRoleKey: keys.serviceRoleKey, - functionsUrl: `https://${ref}.${projectHost}/functions/v1`, - storageBucket, - }, - profilePath, - } satisfies LiveCliProjectEnvironment; - }); - const setupExit = yield* Effect.exit(setup); - if (Exit.isSuccess(setupExit)) return setupExit.value; - - const cleanupExit = yield* Effect.exit(cleanupCreatedProject(api, ref)); - if (Exit.isSuccess(cleanupExit)) return yield* Effect.failCause(setupExit.cause); - return yield* Effect.fail( - cleanupErrors(Cause.squash(setupExit.cause), [Cause.squash(cleanupExit.cause)]), - ); +) => Effect.Effect< + LiveCliProjectEnvironment, + Error, + FileSystem.FileSystem | Path.Path | HttpClient.HttpClient +> = Effect.fn("LiveProject.provision")(function* (api: LiveApi) { + const password = databasePassword(); + const ref = yield* createProject(api, yield* uniqueProjectName, password); + const setup = Effect.gen(function* () { + const project = yield* waitForProject(api, ref); + const projectHost = deriveLiveProjectHost(project.database.host, ref); + const keys = yield* resolveKeys(api, ref); + const dbUrl = yield* resolveDbUrl(api, ref, password); + const storageBucket = "supabase-cli-live-bucket"; + yield* createStorageBucket(ref, projectHost, keys.serviceRoleKey, storageBucket); + const profilePath = yield* writeProfile(ref, projectHost, dbUrl); + return { + project: { + ref, + dbUrl, + dbPassword: password, + anonKey: keys.anonKey, + serviceRoleKey: keys.serviceRoleKey, + functionsUrl: `https://${ref}.${projectHost}/functions/v1`, + storageBucket, + }, + profilePath, + } satisfies LiveCliProjectEnvironment; }); -} + const setupExit = yield* Effect.exit(setup); + if (Exit.isSuccess(setupExit)) return setupExit.value; + + const cleanupExit = yield* Effect.exit(cleanupCreatedProject(api, ref)); + if (Exit.isSuccess(cleanupExit)) return yield* Effect.failCause(setupExit.cause); + return yield* Effect.fail( + cleanupErrors(Cause.squash(setupExit.cause), [Cause.squash(cleanupExit.cause)]), + ); +}); /** Delete the exact owned project and always remove its temporary profile. */ -export function cleanupLiveEnvironment( +export const cleanupLiveEnvironment: ( api: LiveApi, environment: LiveCliProjectEnvironment, -): Effect.Effect { - return Effect.gen(function* () { - const [profileExit, projectExit] = yield* Effect.all( - [ - Effect.exit(cleanupDirectory(environment.profilePath)), - Effect.exit(cleanupRemote(api, environment)), - ], - { concurrency: "unbounded" }, - ); - yield* combineCleanupExits([profileExit, projectExit]); - }); -} +) => Effect.Effect = Effect.fn( + "LiveProject.cleanup", +)(function* (api: LiveApi, environment: LiveCliProjectEnvironment) { + const [profileExit, projectExit] = yield* Effect.all( + [ + Effect.exit(cleanupDirectory(environment.profilePath)), + Effect.exit(cleanupRemote(api, environment)), + ], + { concurrency: "unbounded" }, + ); + yield* combineCleanupExits([profileExit, projectExit]); +}); diff --git a/apps/cli/tests/helpers/live-project.unit.test.ts b/apps/cli/tests/helpers/live-project.unit.test.ts index 9d8b712039..eec23dfa7e 100644 --- a/apps/cli/tests/helpers/live-project.unit.test.ts +++ b/apps/cli/tests/helpers/live-project.unit.test.ts @@ -1,8 +1,8 @@ -import { Effect } from "effect"; +import { describe, expect, it } from "@effect/vitest"; +import { Data, Effect, Exit } from "effect"; import * as HttpClientError from "effect/http/HttpClientError"; import * as HttpClientRequest from "effect/http/HttpClientRequest"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; -import { describe, expect, it } from "vitest"; import { cleanupErrors, @@ -16,6 +16,10 @@ import { type PoolerConfig, } from "./live-project.ts"; +class TemporarilyUnavailable extends Data.TaggedError("TemporarilyUnavailable")<{ + readonly message: string; +}> {} + function statusError(status: number): HttpClientError.HttpClientError { const request = HttpClientRequest.get("https://api.supabase.com/v1/projects/test"); const response = HttpClientResponse.fromWeb(request, new Response(null, { status })); @@ -43,37 +47,37 @@ function poolerConfig(overrides: Partial = {}): PoolerConfig { } describe("live project lifecycle", () => { - it("fails invalid regions before provisioning", async () => { - await expect( - Effect.runPromise(Effect.flip(supportedRegion("not-a-region"))), - ).resolves.toMatchObject({ - message: expect.stringContaining("Unsupported SUPABASE_LIVE_REGION"), - }); - await expect(Effect.runPromise(supportedRegion("us-east-1"))).resolves.toBe("us-east-1"); - }); - - it("retries transient failures until the management operation succeeds", async () => { - let attempts = 0; - const result = await Effect.runPromise( - retryLiveEffect( + it.effect("fails invalid regions before provisioning", () => + Effect.gen(function* () { + expect(yield* Effect.flip(supportedRegion("not-a-region"))).toMatchObject({ + message: expect.stringContaining("Unsupported SUPABASE_LIVE_REGION"), + }); + expect(yield* supportedRegion("us-east-1")).toBe("us-east-1"); + }), + ); + + it.live("retries transient failures until the management operation succeeds", () => + Effect.gen(function* () { + let attempts = 0; + const result = yield* retryLiveEffect( "project readiness", Effect.suspend(() => Effect.sync(() => { attempts += 1; return attempts < 3 - ? Effect.fail(new Error("temporarily unavailable")) + ? Effect.fail(new TemporarilyUnavailable({ message: "temporarily unavailable" })) : Effect.succeed("ACTIVE_HEALTHY"); }).pipe(Effect.flatten), ), { interval: "1 millis", timeout: "100 millis" }, - ), - ); + ); - expect(result).toBe("ACTIVE_HEALTHY"); - expect(attempts).toBe(3); - }); + expect(result).toBe("ACTIVE_HEALTHY"); + expect(attempts).toBe(3); + }), + ); - it("fails a poll when its wall-clock deadline expires", async () => { + it("fails a poll when its wall-clock deadline expires", () => { const result = Effect.runPromise( retryLiveEffect("project keys", Effect.never, { interval: "1 millis", @@ -81,7 +85,7 @@ describe("live project lifecycle", () => { }), ); - await expect(result).rejects.toThrow("project keys timed out"); + return expect(result).rejects.toThrow("project keys timed out"); }); it("preserves both target and cleanup failures", () => { @@ -99,30 +103,28 @@ describe("live project lifecycle", () => { ]); }); - it("retries transient API statuses but fails authorization errors immediately", async () => { - expect(isTransientLiveError(statusError(404))).toBe(true); - expect(isTransientLiveError(statusError(503))).toBe(true); - expect(isTransientLiveError(statusError(401))).toBe(false); - expect(isTransientLiveError(statusError(403))).toBe(false); + it.live("retries transient API statuses but fails authorization errors immediately", () => + Effect.gen(function* () { + expect(isTransientLiveError(statusError(404))).toBe(true); + expect(isTransientLiveError(statusError(503))).toBe(true); + expect(isTransientLiveError(statusError(401))).toBe(false); + expect(isTransientLiveError(statusError(403))).toBe(false); - let attempts = 0; - const transient = statusError(503); - const result = await Effect.runPromise( - retryLiveEffect( + let attempts = 0; + const transient = statusError(503); + const result = yield* retryLiveEffect( "storage bucket", Effect.suspend(() => { attempts += 1; return attempts < 3 ? Effect.fail(transient) : Effect.succeed("created"); }), { interval: "1 millis", timeout: "100 millis", shouldRetry: isTransientLiveError }, - ), - ); - expect(result).toBe("created"); - expect(attempts).toBe(3); + ); + expect(result).toBe("created"); + expect(attempts).toBe(3); - attempts = 0; - await expect( - Effect.runPromise( + attempts = 0; + const exit = yield* Effect.exit( retryLiveEffect( "project readiness", Effect.suspend(() => { @@ -135,10 +137,11 @@ describe("live project lifecycle", () => { shouldRetry: isTransientLiveError, }, ), - ), - ).rejects.toThrow(); - expect(attempts).toBe(1); - }); + ); + expect(Exit.isFailure(exit)).toBe(true); + expect(attempts).toBe(1); + }), + ); it("classifies storage responses for retry without retrying terminal client errors", () => { expect(isTransientStorageStatus(408)).toBe(true); diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index 3a29b8c569..0e54133712 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -1,18 +1,18 @@ -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; - -import { Effect, Predicate } from "effect"; +import { BunFileSystem } from "@effect/platform-bun"; +import { Data, DateTime, Effect, FileSystem, Layer, Predicate, Random, Schema } from "effect"; +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import pg from "pg"; import { expect, inject, test as vitestTest } from "vitest"; import { rootCaBundle } from "../../src/commands/gen/types/types.shared.ts"; import { type CliRunError, - makeTempHome, requireCliSuccess, runSupabase, runSupabaseEffect, + createTempHome, + helperRejection, + type TempHome, } from "./cli.ts"; import { LIVE_EXIT_TIMEOUT_MS, LIVE_SHADOW_PORT } from "./live-env.ts"; import type { LiveCliProjectEnvironment } from "./live-project.ts"; @@ -35,7 +35,7 @@ export interface InvokeResult { export interface LiveFixtures { readonly project: LiveProject; readonly workspace: LiveWorkspace; - readonly home: ReturnType; + readonly home: TempHome; readonly cli: (args: string[], options?: RunOptions) => Promise; readonly cliEffect: ( args: string[], @@ -47,40 +47,129 @@ export interface LiveFixtures { ) => Promise; } +class LiveFixtureError extends Data.TaggedError("LiveFixtureError")<{ + readonly message: string; +}> {} + +class LiveForeignFailure extends Data.TaggedError("LiveForeignFailure")<{ + readonly cause: unknown; +}> {} + +const foreignFailure = (error: { readonly cause?: unknown }) => + new LiveForeignFailure({ cause: error.cause }); + +const awaitForeign = (run: () => Promise) => + Effect.tryPromise({ try: run, catch: (cause) => new LiveForeignFailure({ cause }) }); + +const callForeign = (run: () => A) => + Effect.try({ try: run, catch: (cause) => new LiveForeignFailure({ cause }) }); + +/** + * Runs a helper at its Promise edge, rejecting with a foreign failure's original value and with a + * plain `Error` for a fixture failure. + */ +function runLive(effect: Effect.Effect): Promise { + return Effect.runPromise( + effect.pipe( + Effect.catchTags({ + LiveForeignFailure: (failure) => Effect.die(failure.cause), + LiveFixtureError: (failure) => Effect.die(new Error(failure.message)), + }), + ), + ); +} + +/** `try { body } finally { cleanup }`: a cleanup failure replaces the body's outcome. */ +function withFinally( + body: Effect.Effect, + cleanup: Effect.Effect, +): Effect.Effect { + return Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const exit = yield* Effect.exit(restore(body)); + yield* cleanup; + return yield* exit; + }), + ); +} + +const UnknownFromJsonString = Schema.fromJsonString(Schema.Unknown); + +const invokeFunction = Effect.fnUntraced(function* ( + project: LiveProject, + slug: string, + options?: { readonly anonKey?: string; readonly payload?: unknown }, +) { + const key = options?.anonKey ?? project.anonKey; + const headers: Record = { "Content-Type": "application/json" }; + if (key.length > 0) { + headers["Authorization"] = `Bearer ${key}`; + headers["apikey"] = key; + } + const client = yield* HttpClient.HttpClient; + const request = yield* HttpClientRequest.post(`${project.functionsUrl}/${slug}`).pipe( + HttpClientRequest.setHeaders(headers), + HttpClientRequest.bodyJson(options?.payload ?? {}), + ); + const response = yield* client.execute(request); + const text = yield* response.text; + const body = yield* Schema.decodeEffect(UnknownFromJsonString)(text).pipe( + Effect.orElseSucceed(() => text), + ); + return { status: response.status, body, text }; +}); + const base = vitestTest.extend({ - project: async ({ task: _task }, use) => use(inject("liveProject")), - - home: async ({ task: _task }, use) => { - const home = makeTempHome(); - try { - await use(home); - } finally { - home[Symbol.dispose](); - } - }, - - workspace: async ({ task, home }, use) => { - const suffix = task.name.replace(/[^a-z0-9-]+/giu, "-").slice(0, 40); - const directory = mkdtempSync(path.join(tmpdir(), `supabase-live-${suffix || "test"}-`)); - try { - const initialized = await runSupabase(["init"], { - cwd: directory, - home: home.dir, - env: { SUPABASE_PROFILE: inject("liveProfilePath") }, - }); - if (initialized.exitCode !== 0) { - throw new Error( - `supabase init failed (exit ${initialized.exitCode})\n${initialized.stderr || initialized.stdout}`, + project: ({ task: _task }, use) => use(inject("liveProject")), + + home: ({ task: _task }, use) => + runLive( + /* `try { use } finally { dispose }`: a failed disposal replaces the fixture's outcome. */ + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const home = yield* createTempHome.pipe(Effect.mapError(foreignFailure)); + yield* withFinally( + restore(awaitForeign(() => use(home))), + home.dispose.pipe(Effect.mapError(foreignFailure)), + ); + }), + ), + ), + + workspace: ({ task, home }, use) => + runLive( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const suffix = task.name.replace(/[^a-z0-9-]+/giu, "-").slice(0, 40); + const directory = yield* fs + .makeTempDirectory({ prefix: `supabase-live-${suffix || "test"}-` }) + .pipe(Effect.mapError(foreignFailure)); + yield* withFinally( + Effect.gen(function* () { + const initialized = yield* runSupabaseEffect(["init"], { + cwd: directory, + home: home.dir, + env: { SUPABASE_PROFILE: inject("liveProfilePath") }, + }).pipe( + /* The fixture rejects with what `runSupabase` rejects with for the same failure. */ + Effect.mapError((cause) => new LiveForeignFailure({ cause: helperRejection(cause) })), + ); + if (initialized.exitCode !== 0) { + return yield* new LiveFixtureError({ + message: `supabase init failed (exit ${initialized.exitCode})\n${initialized.stderr || initialized.stdout}`, + }); + } + yield* awaitForeign(() => use({ path: directory })); + }), + fs + .remove(directory, { recursive: true, force: true }) + .pipe(Effect.mapError(foreignFailure)), ); - } - await use({ path: directory }); - } finally { - rmSync(directory, { recursive: true, force: true }); - } - }, + }).pipe(Effect.provide(BunFileSystem.layer)), + ), - cli: async ({ workspace, home }, use) => { - await use((args, options) => + cli: ({ workspace, home }, use) => + use((args, options) => runSupabase(args, { ...options, cwd: options?.cwd ?? workspace.path, @@ -92,11 +181,10 @@ const base = vitestTest.extend({ ...options?.env, }, }), - ); - }, + ), - cliEffect: async ({ workspace, home }, use) => { - await use((args, options) => + cliEffect: ({ workspace, home }, use) => + use((args, options) => runSupabaseEffect(args, { ...options, cwd: options?.cwd ?? workspace.path, @@ -108,32 +196,22 @@ const base = vitestTest.extend({ ...options?.env, }, }), - ); - }, - - invoke: async ({ project }, use) => { - await use(async (slug, options) => { - const key = options?.anonKey ?? project.anonKey; - const headers: Record = { "Content-Type": "application/json" }; - if (key.length > 0) { - headers["Authorization"] = `Bearer ${key}`; - headers["apikey"] = key; - } - const response = await fetch(`${project.functionsUrl}/${slug}`, { - method: "POST", - headers, - body: JSON.stringify(options?.payload ?? {}), - }); - const text = await response.text(); - let body: unknown; - try { - body = JSON.parse(text); - } catch { - body = text; - } - return { status: response.status, body, text }; - }); - }, + ), + + invoke: ({ project }, use) => + use((slug, options) => + runLive( + invokeFunction(project, slug, options).pipe( + Effect.mapError(foreignFailure), + Effect.provide( + Layer.mergeAll( + FetchHttpClient.layer, + Layer.succeed(HttpClient.TracerPropagationEnabled, false), + ), + ), + ), + ), + ), }); /** The sole live fixture. The live global setup owns the shared project. */ @@ -165,17 +243,25 @@ export const storageLiveFlags: ReadonlyArray = ["--linked", "--experimen * remote object, tolerating an already-removed target so teardown stays * idempotent across the moved/renamed paths a test may leave behind. */ -export async function removeStorageLiveObject( +export function removeStorageLiveObject( cli: (args: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>, remote: string, ): Promise { - const removed = await cli(["storage", "rm", remote, "--yes", ...storageLiveFlags]); - if ( - removed.exitCode !== 0 && - !/not found|does not exist/i.test(`${removed.stdout}\n${removed.stderr}`) - ) { - throw new Error(`storage rm cleanup failed:\n${removed.stdout}\n${removed.stderr}`); - } + return runLive( + Effect.gen(function* () { + const removed = yield* awaitForeign(() => + cli(["storage", "rm", remote, "--yes", ...storageLiveFlags]), + ); + if ( + removed.exitCode !== 0 && + !/not found|does not exist/i.test(`${removed.stdout}\n${removed.stderr}`) + ) { + return yield* new LiveFixtureError({ + message: `storage rm cleanup failed:\n${removed.stdout}\n${removed.stderr}`, + }); + } + }), + ); } /** Flags for experimental-gated live tests that address the shared project by @@ -189,38 +275,46 @@ export function experimentalProjectLiveFlags(project: LiveProject): ReadonlyArra * without a database restart. Deleting an absent key is a no-op PUT, so the * teardown stays idempotent. */ -export async function removePostgresConfigLiveOverride( +export function removePostgresConfigLiveOverride( cli: (args: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>, project: LiveProject, key: string, ): Promise { - const removed = await cli([ - "postgres-config", - "delete", - "--config", - key, - ...experimentalProjectLiveFlags(project), - "--no-restart", - ]); - requireCliSuccess(removed, `postgres-config delete cleanup for ${key}`); + return runLive( + Effect.gen(function* () { + const removed = yield* awaitForeign(() => + cli([ + "postgres-config", + "delete", + "--config", + key, + ...experimentalProjectLiveFlags(project), + "--no-restart", + ]), + ); + yield* callForeign(() => + requireCliSuccess(removed, `postgres-config delete cleanup for ${key}`), + ); + }), + ); } /** Exact-version cleanup for migration live tests; reverting an absent row is a no-op delete. */ -export async function removeLiveMigration( +export function removeLiveMigration( cli: LiveFixtures["cli"], project: LiveProject, version: string, ): Promise { - const reverted = await cli([ - "migration", - "repair", - version, - "--status", - "reverted", - "--db-url", - project.dbUrl, - ]); - requireCliSuccess(reverted, `migration repair cleanup for ${version}`); + return runLive( + Effect.gen(function* () { + const reverted = yield* awaitForeign(() => + cli(["migration", "repair", version, "--status", "reverted", "--db-url", project.dbUrl]), + ); + yield* callForeign(() => + requireCliSuccess(reverted, `migration repair cleanup for ${version}`), + ); + }), + ); } /** @@ -229,29 +323,37 @@ export async function removeLiveMigration( * (2s apart, 60s deadline, each attempt bounded) until `key` reads `expected` * (`undefined` for no override). */ -export async function expectPostgresConfigLiveOverride( +export function expectPostgresConfigLiveOverride( cli: LiveFixtures["cli"], project: LiveProject, key: string, expected: string | undefined, label: string, ): Promise { - const read = async (): Promise => { - const proof = await cli( - ["postgres-config", "get", ...experimentalProjectLiveFlags(project), "-o", "json"], - { exitTimeoutMs: 20_000 }, + const read = Effect.gen(function* () { + const proof = yield* awaitForeign(() => + cli(["postgres-config", "get", ...experimentalProjectLiveFlags(project), "-o", "json"], { + exitTimeoutMs: 20_000, + }), ); - requireCliSuccess(proof, label); - const config = requireLiveJson(proof, label); + yield* callForeign(() => requireCliSuccess(proof, label)); + const config = yield* callForeign(() => requireLiveJson(proof, label)); if (!Predicate.isObject(config)) { - throw new Error( - `${label}: unexpected postgres-config get payload\nstdout:\n${proof.stdout}\nstderr:\n${proof.stderr}`, - ); + return yield* new LiveFixtureError({ + message: `${label}: unexpected postgres-config get payload\nstdout:\n${proof.stdout}\nstderr:\n${proof.stderr}`, + }); } return config[key]; - }; - if (Object.is(await read(), expected)) return; - await expect.poll(read, { interval: 2_000, timeout: 60_000, message: label }).toBe(expected); + }); + const readPromise = (): Promise => runLive(read); + /* Polling stays at the Promise edge: vitest's `expect.poll` drives its own reads. */ + return readPromise().then((first) => + Object.is(first, expected) + ? undefined + : expect + .poll(readPromise, { interval: 2_000, timeout: 60_000, message: label }) + .toBe(expected), + ); } /** @@ -260,13 +362,15 @@ export async function expectPostgresConfigLiveOverride( * timestamp version already in the shared project's migration history. */ export function liveMigrationVersion(): string { - const stamp = new Date() - .toISOString() - .replaceAll(/[-:TZ.]/gu, "") - .slice(0, 14); - return `${stamp}${Math.floor(Math.random() * 10_000) - .toString() - .padStart(4, "0")}`; + return Effect.runSync( + Effect.gen(function* () { + const stamp = DateTime.formatIso(yield* DateTime.now) + .replaceAll(/[-:TZ.]/gu, "") + .slice(0, 14); + const suffix = yield* Random.nextIntBetween(0, 10_000, { halfOpen: true }); + return `${stamp}${suffix.toString().padStart(4, "0")}`; + }), + ); } /** @@ -274,27 +378,39 @@ export function liveMigrationVersion(): string { * live assertions can verify database state without invoking another CLI * command. */ -export async function queryLiveDb>( +export function queryLiveDb>( dbUrl: string, query: string, values?: ReadonlyArray, connectionTimeoutMillis?: number, ): Promise { - // Verified TLS against the Supabase CA, so the query works whatever the project's SSL enforcement. - const client = new pg.Client({ - connectionString: dbUrl, - ssl: { ca: rootCaBundle() }, - connectionTimeoutMillis, - }); - // A restart that drops an idle or closing client surfaces only as an `error` event. - client.on("error", () => {}); - await client.connect(); - try { - const result = await client.query(query, values === undefined ? undefined : [...values]); - return result.rows as T[]; - } finally { - await client.end(); - } + return runLive( + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + // Verified TLS against the Supabase CA, so the query works whatever the project's SSL enforcement. + const client = yield* callForeign( + () => + new pg.Client({ + connectionString: dbUrl, + ssl: { ca: rootCaBundle() }, + connectionTimeoutMillis, + }), + ); + // A restart that drops an idle or closing client surfaces only as an `error` event. + client.on("error", () => {}); + yield* restore(awaitForeign(() => client.connect())); + /* `try { query } finally { end }`: a failed `end()` replaces the query's outcome. */ + return yield* withFinally( + restore( + awaitForeign(() => + client.query(query, values === undefined ? undefined : [...values]), + ).pipe(Effect.map((result) => result.rows)), + ), + awaitForeign(() => client.end()), + ); + }), + ), + ); } /** Rethrow a target failure without discarding failures from exact cleanup. */ diff --git a/apps/cli/tests/helpers/local-reset.ts b/apps/cli/tests/helpers/local-reset.ts index 361f355860..429c96b195 100644 --- a/apps/cli/tests/helpers/local-reset.ts +++ b/apps/cli/tests/helpers/local-reset.ts @@ -32,14 +32,12 @@ export function mockContainerCliSpawner(route: (args: ReadonlyArray) => spawned.push({ args }); if (command._tag !== "StandardCommand") { - return yield* Effect.fail( - PlatformError.systemError({ - _tag: "NotFound", - module: "ChildProcess", - method: "spawn", - description: "spawn failed", - }), - ); + return yield* PlatformError.systemError({ + _tag: "NotFound", + module: "ChildProcess", + method: "spawn", + description: "spawn failed", + }); } const result = route(args); diff --git a/apps/cli/tests/helpers/macos-signature.ts b/apps/cli/tests/helpers/macos-signature.ts index 8663ee076f..5f91e23b41 100644 --- a/apps/cli/tests/helpers/macos-signature.ts +++ b/apps/cli/tests/helpers/macos-signature.ts @@ -1,8 +1,7 @@ -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { Effect, FileSystem, Path } from "effect"; +import type { PlatformError } from "effect/PlatformError"; import { macIdentifierFor } from "../../scripts/macos-signing.ts"; -import { runCli } from "./release-shell.ts"; +import { jsonQuote, type ReleaseStepError, runCliEffect } from "./release-shell.ts"; export type SignatureCheckResult = { readonly passed: boolean; @@ -18,23 +17,30 @@ export type SignatureCheckResult = { * (Phase 2), it additionally checks the hardened-runtime flag and that Gatekeeper accepts a * quarantined copy, which validates the online notarization ticket. */ -export async function verifyMacSignature(binPath: string): Promise { +export const verifyMacSignature = Effect.fn("MacosSignature.verifyMacSignature")(function* ( + binPath: string, +): Effect.fn.Return< + SignatureCheckResult, + ReleaseStepError | PlatformError, + FileSystem.FileSystem | Path.Path +> { + const path = yield* Path.Path; const binary = path.basename(binPath); const expectedId = macIdentifierFor(binary); if (!expectedId) { return { passed: false, detail: `no expected identifier configured for ${binary}` }; } - const verify = await runCli("codesign", ["--verify", "--strict", "--verbose=2", binPath]); + const verify = yield* runCliEffect("codesign", ["--verify", "--strict", "--verbose=2", binPath]); if (verify.exitCode !== 0) { return { passed: false, - detail: `codesign --verify --strict failed: exit=${verify.exitCode}, stderr=${JSON.stringify(verify.stderr)}`, + detail: `codesign --verify --strict failed: exit=${verify.exitCode}, stderr=${yield* jsonQuote(verify.stderr)}`, }; } // codesign writes the signature display to stderr. - const display = await runCli("codesign", ["-dvv", binPath]); + const display = yield* runCliEffect("codesign", ["-dvv", binPath]); const info = [display.stdout, display.stderr].filter(Boolean).join("\n"); // Match the whole identifier value (codesign prints `Identifier=` on its @@ -63,8 +69,8 @@ export async function verifyMacSignature(binPath: string): Promise { - const dir = await mkdtemp(path.join(tmpdir(), "supabase-gatekeeper-")); - const copy = path.join(dir, path.basename(binPath)); - try { - await runCli("cp", [binPath, copy]); - await runCli("xattr", ["-w", "com.apple.quarantine", "0081;00000000;smoke;", copy]); - const assess = await runCli("spctl", ["--assess", "--type", "execute", "--verbose=2", copy]); - if (assess.exitCode !== 0) { +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const assessCopy = (dir: string) => + Effect.gen(function* () { + const copy = path.join(dir, path.basename(binPath)); + yield* runCliEffect("cp", [binPath, copy]); + yield* runCliEffect("xattr", ["-w", "com.apple.quarantine", "0081;00000000;smoke;", copy]); + const assess = yield* runCliEffect("spctl", [ + "--assess", + "--type", + "execute", + "--verbose=2", + copy, + ]); + if (assess.exitCode !== 0) { + return { + passed: false, + detail: `spctl rejected quarantined binary: exit=${assess.exitCode}, stderr=${yield* jsonQuote(assess.stderr)}`, + }; + } return { - passed: false, - detail: `spctl rejected quarantined binary: exit=${assess.exitCode}, stderr=${JSON.stringify(assess.stderr)}`, + passed: true, + detail: `Developer ID + notarized signature ok (Identifier=${expectedId})`, }; - } - return { - passed: true, - detail: `Developer ID + notarized signature ok (Identifier=${expectedId})`, - }; - } finally { - await rm(dir, { recursive: true, force: true }); - } -} + }); + /* `try { assess } finally { remove }`: a failed removal replaces the assessment's outcome. */ + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const dir = yield* fs.makeTempDirectory({ prefix: "supabase-gatekeeper-" }); + const outcome = yield* Effect.exit(restore(assessCopy(dir))); + yield* fs.remove(dir, { recursive: true, force: true }); + return yield* outcome; + }), + ); +}); diff --git a/apps/cli/tests/helpers/mocks.ts b/apps/cli/tests/helpers/mocks.ts index 072f1d594a..ed5de4d4e9 100644 --- a/apps/cli/tests/helpers/mocks.ts +++ b/apps/cli/tests/helpers/mocks.ts @@ -1,8 +1,19 @@ import { tmpdir } from "node:os"; -import { join } from "node:path"; import process from "node:process"; -import { BunServices } from "@effect/platform-bun"; -import { Console, ConfigProvider, Deferred, Effect, Layer, Option, Stream } from "effect"; +import { BunPath, BunServices } from "@effect/platform-bun"; +import { + Console, + ConfigProvider, + DateTime, + Deferred, + Effect, + Layer, + Option, + Path, + Random, + Schema, + Stream, +} from "effect"; import type { CliProjectEnvironment, CliProjectPaths } from "@supabase/config"; import { testRunEnvVar as stackTestRunEnvVar } from "@supabase/stack/internal/test-run-label"; import { cliSettingsLayer } from "../../src/shared/config/cli-settings.layer.ts"; @@ -51,16 +62,33 @@ type OutputEvent = { // created on disk, so a test combining it with a real FileSystem layer can't collide // with another run's files. Tests that read/write real files under homeDir should use // `useTempWorkdir` in `command-mocks.ts` instead. -const defaultTestHomeDir = join( - tmpdir(), - `supabase-cli-test-home-${process.pid.toString(36)}-${Math.random().toString(36).slice(2, 8)}`, +const { defaultTestHomeDir, defaultTestConfigDir } = Effect.runSync( + Effect.gen(function* () { + const path = yield* Path.Path; + const suffix = (yield* Random.next).toString(36).slice(2, 8); + const homeDir = path.join( + tmpdir(), + `supabase-cli-test-home-${process.pid.toString(36)}-${suffix}`, + ); + return { defaultTestHomeDir: homeDir, defaultTestConfigDir: path.join(homeDir, ".supabase") }; + }).pipe(Effect.provide(BunPath.layer)), ); -export function mockBrowser(): Layer.Layer { - return Layer.succeed(Browser, { - open: () => Effect.void, - }); -} +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + +/* JSON.stringify returns undefined for undefined, functions and symbols; those print "undefined". */ +const jsonText = (value: unknown): Effect.Effect => + encodeJson(value).pipe( + Effect.catch((error) => + value === undefined || typeof value === "function" || typeof value === "symbol" + ? Effect.succeed("undefined") + : Effect.die(error), + ), + ); + +export const mockBrowser: Layer.Layer = Layer.succeed(Browser, { + open: () => Effect.void, +}); export function mockStdin(isTTY: boolean, pipedInput?: string | Uint8Array): Layer.Layer { const pipedBytes = @@ -286,25 +314,25 @@ export function mockOutput( }; }), event: (event) => - Effect.sync(() => { + Effect.gen(function* () { events.push(event as OutputEvent); messages.push({ type: "info", message: event.type === "log-entry" ? `[${event.service}] ${event.line}` - : JSON.stringify(event), + : yield* jsonText(event), }); }), result: (data: unknown) => - Effect.sync(() => { + Effect.gen(function* () { if (opts.format === "json") { - rawChunks.push({ text: `${JSON.stringify(data)}\n`, stream: "stdout" }); + rawChunks.push({ text: `${yield* jsonText(data)}\n`, stream: "stdout" }); } else if (opts.format === "stream-json") { events.push({ type: "result", data, - timestamp: new Date().toISOString(), + timestamp: DateTime.formatIso(DateTime.nowUnsafe()), }); } }), @@ -534,7 +562,7 @@ export function mockTelemetryRuntime( return Layer.succeed( TelemetryRuntime, TelemetryRuntime.of({ - configDir: opts.configDir ?? join(defaultTestHomeDir, ".supabase"), + configDir: opts.configDir ?? defaultTestConfigDir, consent: opts.consent ?? "granted", deviceId: opts.deviceId ?? "test-device-id", sessionId: opts.sessionId ?? "test-session-id", @@ -549,34 +577,43 @@ export function mockTelemetryRuntime( ); } -function applyProcessEnv(values: Readonly>) { - const snapshot = { ...process.env }; +function applyProcessEnv( + env: NodeJS.ProcessEnv, + values: Readonly>, +) { + const snapshot = { ...env }; - for (const key of Object.keys(process.env)) { - delete process.env[key]; + for (const key of Object.keys(env)) { + delete env[key]; } for (const [key, value] of Object.entries(values)) { if (value !== undefined) { - process.env[key] = value; + env[key] = value; } } return snapshot; } +/* Keeps the ambient stack test-run label unless `values` pins its own. */ +function withAmbientTestRun( + env: NodeJS.ProcessEnv, + values: Readonly>, +): Readonly> { + const ambientTestRun = env[stackTestRunEnvVar]; + return stackTestRunEnvVar in values || ambientTestRun === undefined + ? values + : { [stackTestRunEnvVar]: ambientTestRun, ...values }; +} + export function processEnvLayer( values: Readonly> = {}, ): Layer.Layer { return ConfigProvider.layer( Effect.acquireRelease( Effect.sync(() => { - const ambientTestRun = process.env[stackTestRunEnvVar]; - const snapshot = applyProcessEnv( - stackTestRunEnvVar in values || ambientTestRun === undefined - ? values - : { [stackTestRunEnvVar]: ambientTestRun, ...values }, - ); + const snapshot = applyProcessEnv(process.env, withAmbientTestRun(process.env, values)); return { provider: ConfigProvider.fromEnvRecord(process.env, { preserveEmptyStrings: true }), snapshot, @@ -584,7 +621,7 @@ export function processEnvLayer( }), ({ snapshot }) => Effect.sync(() => { - applyProcessEnv(snapshot); + applyProcessEnv(process.env, snapshot); }), ).pipe(Effect.map(({ provider }) => provider)), ); @@ -660,10 +697,10 @@ function mockProjectLinkState( ); } -export function emptyEnv() { +export function emptyEnv(env: Readonly> = {}) { const runtimeInfoLayer = mockRuntimeInfo(); const cliProjectContextLayer = mockCliProjectContext(); - const envLayer = processEnvLayer(); + const envLayer = processEnvLayer(env); const cliProjectHomeLayer = mockCliProjectHome(); const projectLinkStateLayer = mockProjectLinkState(); const analytics = mockAnalytics(); diff --git a/apps/cli/tests/helpers/npm-registry.ts b/apps/cli/tests/helpers/npm-registry.ts index b1dd6c0dd5..17597ac0f1 100644 --- a/apps/cli/tests/helpers/npm-registry.ts +++ b/apps/cli/tests/helpers/npm-registry.ts @@ -1,21 +1,30 @@ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; import { - lstat, - mkdir, - mkdtemp, - readFile, - readdir, - readlink, - rm, - stat, - writeFile, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; + Cause, + Clock, + Console, + Data, + Effect, + Exit, + FileSystem, + Layer, + Option, + Path, + Predicate, + Schedule, + Schema, +} from "effect"; +import { FetchHttpClient, HttpClient } from "effect/http"; +import type { PlatformError } from "effect/PlatformError"; import process from "node:process"; -import { runCli, verifyExpectedShell } from "./release-shell.ts"; - -const root = path.resolve(import.meta.dir, "../../../.."); +import { + jsonQuote, + ReleaseStepError, + releaseStep, + runCliEffect, + verifyExpectedShellEffect, +} from "./release-shell.ts"; const PACKAGE_PATHS = { "cli-darwin-arm64": ["packages", "cli-darwin-arm64"], @@ -31,173 +40,304 @@ const PACKAGE_PATHS = { const ALL_PACKAGES = Object.keys(PACKAGE_PATHS) as Array; -export async function createTmpDir(prefix: string): Promise { - const dir = await mkdtemp(path.join(tmpdir(), prefix)); - return { - path: dir, - async [Symbol.asyncDispose]() { - await rm(dir, { recursive: true }); - }, - }; +/** The local Verdaccio registry never answered its ping. */ +class VerdaccioStartTimeout extends Data.TaggedError("VerdaccioStartTimeout")<{ + readonly message: string; +}> {} + +type RegistryServices = FileSystem.FileSystem | Path.Path | HttpClient.HttpClient; + +const registryLayer: Layer.Layer = Layer.mergeAll( + BunFileSystem.layer, + BunPath.layer, + FetchHttpClient.layer, + Layer.succeed(HttpClient.TracerPropagationEnabled, false), +); + +/** A resource release failed after its use had already failed; `suppressed` is the use's cause. */ +class DisposalSuppressedError extends Data.TaggedError("DisposalSuppressedError")<{ + readonly error: unknown; + readonly suppressed: Cause.Cause; +}> {} + +type RegistryFailure = + | ReleaseStepError + | PlatformError + | VerdaccioStartTimeout + | Schema.SchemaError + | DisposalSuppressedError; + +/* What a facade rejects with: the original error, or a SuppressedError as `await using` builds. */ +function thrownValue(error: unknown): unknown { + if (error instanceof ReleaseStepError) return error.cause; + if (error instanceof VerdaccioStartTimeout) return new Error(error.message); + if (error instanceof DisposalSuppressedError) { + return new SuppressedError( + thrownValue(error.error), + thrownValue(Cause.squash(error.suppressed)), + ); + } + if (Predicate.isTagged(error, "PlatformError") && Predicate.hasProperty(error, "cause")) { + return error.cause ?? error; + } + return error; } -async function startVerdaccio( - configPath: string, - port: number, -): Promise { - const url = `http://localhost:${port}`; - const proc = Bun.spawn(["bunx", "verdaccio", "--config", configPath], { - stdout: "ignore", - stderr: "ignore", +/* Promise facades reject with the original thrown value. */ +const runRegistry = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe( + Effect.catch((error) => Effect.die(thrownValue(error))), + Effect.provide(registryLayer), + ), + ); + +/** Uses then releases a resource like `await using`: both failures surface when use and release fail. */ +const usingResource = ( + acquire: Effect.Effect, + use: (resource: A) => Effect.Effect, + release: (resource: A) => Effect.Effect, +) => + Effect.acquireUseRelease( + acquire, + (resource) => Effect.exit(use(resource)), + (resource, used) => + release(resource).pipe( + Effect.mapError((error) => + Exit.isSuccess(used) && Exit.isFailure(used.value) + ? new DisposalSuppressedError({ error, suppressed: used.value.cause }) + : error, + ), + ), + ).pipe(Effect.flatten); + +/* Reads UTF-8 text and keeps a leading byte order mark, like `readFile(path, "utf-8")`. */ +const utf8 = new TextDecoder("utf-8", { ignoreBOM: true }); +const readUtf8 = (fs: FileSystem.FileSystem, pathname: string) => + fs.readFile(pathname).pipe(Effect.map((bytes) => utf8.decode(bytes))); + +const repoRoot = Effect.gen(function* () { + const path = yield* Path.Path; + return path.resolve(import.meta.dir, "../../../.."); +}); + +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + +const decodePackageIdentity = Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Struct({ + name: Schema.optional(Schema.Unknown), + version: Schema.optional(Schema.Unknown), + }), + ), +); + +const decodePackageName = Schema.decodeUnknownEffect( + Schema.fromJsonString(Schema.Struct({ name: Schema.String })), +); + +/* Renders a caught platform failure as `${error}` renders its host error. */ +const foreignText = (error: PlatformError) => + error.cause instanceof Error ? error.cause.toString() : error.toString(); + +const isNotFound = (error: PlatformError) => Predicate.isTagged(error.reason, "NotFound"); + +export function createTmpDir(prefix: string): Promise { + return runRegistry( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const dir = yield* fs.makeTempDirectory({ prefix }); + return { + path: dir, + [Symbol.asyncDispose]: () => runRegistry(fs.remove(dir, { recursive: true })), + }; + }), + ); +} + +const spawnVerdaccio = (configPath: string) => + Effect.try({ + try: () => + Bun.spawn(["bunx", "verdaccio", "--config", configPath], { + stdout: "ignore", + stderr: "ignore", + }), + catch: (cause) => new ReleaseStepError({ cause }), }); +type PingState = "ready" | "pending" | "expired"; + +const awaitVerdaccio = Effect.fnUntraced(function* (url: string) { const timeout = 120_000; - const deadline = Date.now() + timeout; - while (Date.now() < deadline) { - try { - const res = await fetch(`${url}/-/ping`); - if (res.ok) return { url, [Symbol.asyncDispose]: async () => proc.kill() }; - } catch {} - await Bun.sleep(500); - } + const deadline = (yield* Clock.currentTimeMillis) + timeout; + /* Each round checks the deadline before it pings; an unready ping waits 500 ms for the next. */ + const ping = Effect.flatMap( + Clock.currentTimeMillis, + (now): Effect.Effect => + now < deadline + ? HttpClient.get(`${url}/-/ping`).pipe( + Effect.map((res): PingState => + res.status >= 200 && res.status < 300 ? "ready" : "pending", + ), + Effect.orElseSucceed((): PingState => "pending"), + ) + : Effect.succeed("expired"), + ); + const state = yield* ping.pipe( + Effect.repeat({ schedule: Schedule.spaced(500), while: (current) => current === "pending" }), + ); + if (state === "ready") return; - proc.kill(); - throw new Error(`Verdaccio failed to start within ${timeout / 1000}s`); -} + return yield* new VerdaccioStartTimeout({ + message: `Verdaccio failed to start within ${timeout / 1000}s`, + }); +}); -async function savePackageJsons() { +const savePackageJsons = Effect.fnUntraced(function* (root: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const originals = new Map(); for (const pkg of ALL_PACKAGES) { const p = path.join(root, ...PACKAGE_PATHS[pkg], "package.json"); - originals.set(p, await readFile(p, "utf-8")); + originals.set(p, yield* readUtf8(fs, p)); } - return { - async [Symbol.asyncDispose]() { - for (const [p, content] of originals) { - await writeFile(p, content); - } - }, - }; -} + return originals; +}); + +const restorePackageJsons = Effect.fnUntraced(function* (originals: ReadonlyMap) { + const fs = yield* FileSystem.FileSystem; + for (const [p, content] of originals) { + yield* fs.writeFileString(p, content); + } +}); function modeOctal(mode: number): string { return `0${(mode & 0o777).toString(8).padStart(3, "0")}`; } -async function describePath(p: string): Promise { - try { - const ls = await lstat(p); - if (ls.isSymbolicLink()) { - const target = await readlink(p); - try { - const st = await stat(p); - return `symlink ${modeOctal(ls.mode)} -> ${target} (target ${modeOctal(st.mode)} ${st.size}B)`; - } catch (e) { - return `symlink ${modeOctal(ls.mode)} -> ${target} (target unreadable: ${e})`; - } - } - return `file ${modeOctal(ls.mode)} ${ls.size}B`; - } catch (e) { - if ((e as NodeJS.ErrnoException).code === "ENOENT") return "MISSING"; - return `unstattable: ${e}`; +/* `FileSystem` has no `lstat`: a readable link marks a symlink, and the link's own mode is unknown. */ +const describePath = Effect.fnUntraced(function* (p: string) { + const fs = yield* FileSystem.FileSystem; + const linkTarget = yield* fs.readLink(p).pipe(Effect.option); + if (Option.isSome(linkTarget)) { + return yield* fs.stat(p).pipe( + Effect.map( + (st) => `symlink -> ${linkTarget.value} (target ${modeOctal(st.mode)} ${st.size}B)`, + ), + Effect.catch((e) => + Effect.succeed(`symlink -> ${linkTarget.value} (target unreadable: ${foreignText(e)})`), + ), + ); } -} + return yield* fs.stat(p).pipe( + Effect.map((st) => `file ${modeOctal(st.mode)} ${st.size}B`), + Effect.catch((e) => + Effect.succeed(isNotFound(e) ? "MISSING" : `unstattable: ${foreignText(e)}`), + ), + ); +}); -async function dumpInstalledTree(testDir: string, ext: string): Promise { - console.log("\nInstalled tree state:"); +const dumpInstalledTree = Effect.fnUntraced(function* (testDir: string, ext: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* Console.log("\nInstalled tree state:"); const interesting = [ path.join(testDir, "node_modules", ".bin", `supabase${ext}`), path.join(testDir, "node_modules", "supabase", "package.json"), path.join(testDir, "node_modules", "supabase", "dist", "supabase.js"), ]; for (const p of interesting) { - console.log(` ${path.relative(testDir, p)}: ${await describePath(p)}`); + yield* Console.log(` ${path.relative(testDir, p)}: ${yield* describePath(p)}`); } const supabaseScope = path.join(testDir, "node_modules", "@supabase"); - let scopeEntries: string[] = []; - try { - scopeEntries = await readdir(supabaseScope); - } catch { - console.log(` node_modules/@supabase: MISSING`); + const scopeEntries = yield* fs.readDirectory(supabaseScope).pipe(Effect.option); + if (Option.isNone(scopeEntries)) { + yield* Console.log(` node_modules/@supabase: MISSING`); return; } - for (const entry of scopeEntries.sort()) { + for (const entry of scopeEntries.value.sort()) { const pkgDir = path.join(supabaseScope, entry); const pkgJsonPath = path.join(pkgDir, "package.json"); - try { - const pkgJson = JSON.parse(await readFile(pkgJsonPath, "utf-8")); - console.log(` node_modules/@supabase/${entry}: ${pkgJson.name}@${pkgJson.version}`); - } catch { - console.log(` node_modules/@supabase/${entry}: `); - } + yield* readUtf8(fs, pkgJsonPath).pipe( + Effect.flatMap(decodePackageIdentity), + Effect.flatMap((pkgJson) => + Console.log( + ` node_modules/@supabase/${entry}: ${String(pkgJson.name)}@${String(pkgJson.version)}`, + ), + ), + Effect.catch(() => + Console.log(` node_modules/@supabase/${entry}: `), + ), + ); const binDir = path.join(pkgDir, "bin"); - try { - const binEntries = await readdir(binDir); - for (const b of binEntries.sort()) { + const binEntries = yield* fs.readDirectory(binDir).pipe(Effect.option); + if (Option.isSome(binEntries)) { + for (const b of binEntries.value.sort()) { const bp = path.join(binDir, b); - console.log(` bin/${b}: ${await describePath(bp)}`); + yield* Console.log(` bin/${b}: ${yield* describePath(bp)}`); } - } catch {} + } } -} +}); -async function findPlatformBinary(testDir: string, ext: string): Promise { +const findPlatformBinary = Effect.fnUntraced(function* (testDir: string, ext: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const supabaseScope = path.join(testDir, "node_modules", "@supabase"); - let entries: string[] = []; - try { - entries = await readdir(supabaseScope); - } catch { + const entries = yield* fs.readDirectory(supabaseScope).pipe(Effect.option); + if (Option.isNone(entries)) { return null; } - for (const entry of entries) { + for (const entry of entries.value) { const candidate = path.join(supabaseScope, entry, "bin", `supabase${ext}`); - try { - await stat(candidate); - return candidate; - } catch {} + const found = yield* fs.stat(candidate).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ); + if (found) return candidate; } return null; -} +}); -async function inspectVerdaccioTarball(storageDir: string, pkg: string): Promise { +const inspectVerdaccioTarball = Effect.fnUntraced(function* (storageDir: string, pkg: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const pkgStorage = path.join(storageDir, "@supabase", pkg); - let files: string[] = []; - try { - files = await readdir(pkgStorage); - } catch { - console.log(` @supabase/${pkg}: `); + const files = yield* fs.readDirectory(pkgStorage).pipe(Effect.option); + if (Option.isNone(files)) { + yield* Console.log(` @supabase/${pkg}: `); return; } - const tarball = files.find((f) => f.endsWith(".tgz")); + const tarball = files.value.find((f) => f.endsWith(".tgz")); if (!tarball) { - console.log(` @supabase/${pkg}: `); + yield* Console.log(` @supabase/${pkg}: `); return; } const tarballPath = path.join(pkgStorage, tarball); - const listing = await $`tar -tvf ${tarballPath}`.text(); + const listing = yield* releaseStep(() => $`tar -tvf ${tarballPath}`.text()); // Surface only the bin entries — full listings drown the log. const binLines = listing .split("\n") .filter((line) => line.includes("/bin/")) .map((line) => ` ${line.trim()}`); if (binLines.length === 0) { - console.log(` @supabase/${pkg} (${tarball}): `); + yield* Console.log(` @supabase/${pkg} (${tarball}): `); return; } - console.log(` @supabase/${pkg} (${tarball}):`); - for (const line of binLines) console.log(line); -} + yield* Console.log(` @supabase/${pkg} (${tarball}):`); + for (const line of binLines) yield* Console.log(line); +}); -async function hasVerdaccioTarball(storageDir: string, pkg: string): Promise { +const hasVerdaccioTarball = Effect.fnUntraced(function* (storageDir: string, pkg: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const pkgStorage = path.join(storageDir, "@supabase", pkg); - try { - const files = await readdir(pkgStorage); - return files.some((f) => f.endsWith(".tgz")); - } catch { - return false; - } -} + return yield* fs.readDirectory(pkgStorage).pipe( + Effect.map((files) => files.some((f) => f.endsWith(".tgz"))), + Effect.orElseSucceed(() => false), + ); +}); function outputText(value: unknown): string { if (typeof value === "string") return value; @@ -217,23 +357,43 @@ export function describeError(e: unknown): string { return String(e); } -export async function runNpmTest(version: string, tag = "latest"): Promise { - await using _pkgJsons = await savePackageJsons(); - await using tmp = await createTmpDir("npm-smoke-"); +const runNpmTestEffect = Effect.fnUntraced(function* (version: string, tag: string) { + const root = yield* repoRoot; + const fs = yield* FileSystem.FileSystem; + return yield* usingResource( + savePackageJsons(root), + () => + usingResource( + fs.makeTempDirectory({ prefix: "npm-smoke-" }), + (tmpPath) => publishAndVerify(root, tmpPath, version, tag), + (tmpPath) => fs.remove(tmpPath, { recursive: true }), + ), + restorePackageJsons, + ); +}); + +const publishAndVerify = Effect.fnUntraced(function* ( + root: string, + tmpPath: string, + version: string, + tag: string, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const PORT = 4873; - const configPath = path.join(tmp.path, "config.yaml"); - const storageDir = path.join(tmp.path, "storage"); + const configPath = path.join(tmpPath, "config.yaml"); + const storageDir = path.join(tmpPath, "storage"); // Stores our published tarballs; the umbrella package's `@supabase/cli-*` // optional deps resolve here, while the public npm uplink stays available // for npm installer internals and other tooling. - await writeFile( + yield* fs.writeFileString( configPath, `storage: ${storageDir} auth: htpasswd: - file: ${path.join(tmp.path, "htpasswd")} + file: ${path.join(tmpPath, "htpasswd")} max_users: 100 uplinks: npmjs: @@ -257,81 +417,113 @@ listen: 0.0.0.0:${PORT} // pnpm publish delegates to npm internals, which only honor per-registry auth // in an .npmrc (not `NPM_CONFIG_TOKEN`); write one and point npm at it via // `npm_config_userconfig`. - const publishNpmrc = path.join(tmp.path, "publish.npmrc"); - await writeFile(publishNpmrc, `//localhost:${PORT}/:_authToken=dummy\n`); + const publishNpmrc = path.join(tmpPath, "publish.npmrc"); + yield* fs.writeFileString(publishNpmrc, `//localhost:${PORT}/:_authToken=dummy\n`); const publishEnv = { ...process.env, npm_config_userconfig: publishNpmrc }; // Sync versions across all packages - console.log(`Syncing versions to ${version}...`); - await $`bun ../../scripts/sync-versions.ts --version ${version}`.cwd(import.meta.dir).quiet(); + yield* Console.log(`Syncing versions to ${version}...`); + yield* releaseStep(() => + $`bun ../../scripts/sync-versions.ts --version ${version}`.cwd(import.meta.dir).quiet(), + ); - console.log("Starting local npm registry..."); - await using registry = await startVerdaccio(configPath, PORT); - console.log(`Registry ready at ${registry.url}\n`); + yield* Console.log("Starting local npm registry..."); + const registryUrl = `http://localhost:${PORT}`; + return yield* Effect.acquireUseRelease( + spawnVerdaccio(configPath), + () => + awaitVerdaccio(registryUrl).pipe( + Effect.andThen( + verifyWithRegistry(root, tmpPath, storageDir, registryUrl, PORT, tag, publishEnv), + ), + ), + (proc) => Effect.sync(() => proc.kill()), + ); +}); + +const verifyWithRegistry = Effect.fnUntraced(function* ( + root: string, + tmpPath: string, + storageDir: string, + registryUrl: string, + PORT: number, + tag: string, + publishEnv: Record, +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* Console.log(`Registry ready at ${registryUrl}\n`); const platformPackages = ALL_PACKAGES.filter((p) => p !== "cli"); - console.log("Publishing platform packages..."); + yield* Console.log("Publishing platform packages..."); for (const pkg of platformPackages) { const pkgDir = path.join(root, "packages", pkg); - try { - await $`pnpm publish --registry ${registry.url} --tag ${tag} --no-git-checks` + const published = yield* releaseStep(() => + $`pnpm publish --registry ${registryUrl} --tag ${tag} --no-git-checks` .cwd(pkgDir) - .env(publishEnv); - console.log(` @supabase/${pkg}`); - } catch (e) { - if (await hasVerdaccioTarball(storageDir, pkg)) { - console.log(` @supabase/${pkg} (already present in local registry)`); - continue; - } - throw e; - } + .env(publishEnv), + ).pipe( + Effect.as(true), + Effect.catch((e) => + Effect.flatMap(hasVerdaccioTarball(storageDir, pkg), (present) => + present ? Effect.succeed(false) : Effect.fail(e), + ), + ), + ); + yield* Console.log( + published ? ` @supabase/${pkg}` : ` @supabase/${pkg} (already present in local registry)`, + ); } // Confirms `publishConfig.executableFiles` made it into the published tarball. - console.log("\nVerdaccio tarball contents (bin entries only):"); + yield* Console.log("\nVerdaccio tarball contents (bin entries only):"); for (const pkg of platformPackages) { - await inspectVerdaccioTarball(storageDir, pkg); + yield* inspectVerdaccioTarball(storageDir, pkg); } const cliDir = path.join(root, "apps", "cli"); - console.log("\nBuilding umbrella package shim..."); - await $`pnpm build:shim`.cwd(cliDir).quiet(); + yield* Console.log("\nBuilding umbrella package shim..."); + yield* releaseStep(() => $`pnpm build:shim`.cwd(cliDir).quiet()); - const cliPkgJson = await readFile(path.join(cliDir, "package.json"), "utf-8").then(JSON.parse); + const cliPkgJson = yield* readUtf8(fs, path.join(cliDir, "package.json")).pipe( + Effect.flatMap(decodePackageName), + ); const umbrellaName: string = cliPkgJson.name; - console.log("Publishing umbrella package..."); - await $`pnpm publish --registry ${registry.url} --tag ${tag} --no-git-checks` - .cwd(cliDir) - .env(publishEnv); - console.log(` ${umbrellaName}\n`); + yield* Console.log("Publishing umbrella package..."); + yield* releaseStep(() => + $`pnpm publish --registry ${registryUrl} --tag ${tag} --no-git-checks` + .cwd(cliDir) + .env(publishEnv), + ); + yield* Console.log(` ${umbrellaName}\n`); - console.log("Verdaccio umbrella tarball contents:"); + yield* Console.log("Verdaccio umbrella tarball contents:"); const umbrellaStorage = path.join(storageDir, umbrellaName); - try { - const files = await readdir(umbrellaStorage); + yield* Effect.gen(function* () { + const files = yield* fs.readDirectory(umbrellaStorage); const tarball = files.find((f) => f.endsWith(".tgz")); if (tarball) { - const listing = await $`tar -tvf ${path.join(umbrellaStorage, tarball)}`.text(); + const listing = yield* releaseStep(() => + $`tar -tvf ${path.join(umbrellaStorage, tarball)}`.text(), + ); for (const line of listing.split("\n").filter(Boolean)) { - console.log(` ${line.trim()}`); + yield* Console.log(` ${line.trim()}`); } } else { - console.log(` `); + yield* Console.log(` `); } - } catch { - console.log(` `); - } + }).pipe(Effect.catch(() => Console.log(` `))); - const testDir = path.join(tmp.path, "test-project"); - await mkdir(testDir); - await writeFile( + const testDir = path.join(tmpPath, "test-project"); + yield* fs.makeDirectory(testDir); + yield* fs.writeFileString( path.join(testDir, "package.json"), - JSON.stringify({ name: "test-npm-smoke", version: "0.0.0", private: true }), + yield* encodeJson({ name: "test-npm-smoke", version: "0.0.0", private: true }), ); - await writeFile( + yield* fs.writeFileString( path.join(testDir, ".npmrc"), - `registry=${registry.url}\n//localhost:${PORT}/:_authToken=dummy\n`, + `registry=${registryUrl}\n//localhost:${PORT}/:_authToken=dummy\n`, ); // Pass --registry explicitly: pnpm-set `npm_config_*` env vars can override the @@ -339,45 +531,49 @@ listen: 0.0.0.0:${PORT} // the published CLI rather than the umbrella just packed. The CLI flag wins over // both env vars and .npmrc. const installSpec = tag === "latest" ? umbrellaName : `${umbrellaName}@${tag}`; - console.log(`\nInstalling ${installSpec}...`); - await $`npm install --registry ${registry.url} ${installSpec}`.cwd(testDir); + yield* Console.log(`\nInstalling ${installSpec}...`); + yield* releaseStep(() => $`npm install --registry ${registryUrl} ${installSpec}`.cwd(testDir)); - console.log("\nVerifying..."); + yield* Console.log("\nVerifying..."); const ext = process.platform === "win32" ? ".cmd" : ""; const binPath = path.join(testDir, "node_modules", ".bin", `supabase${ext}`); - await dumpInstalledTree(testDir, ext); + yield* dumpInstalledTree(testDir, ext); - const versionResult = await runCli(binPath, ["--version"]); + const versionResult = yield* runCliEffect(binPath, ["--version"]); const hasValidVersion = versionResult.exitCode === 0 && /^\d+\.\d+\.\d+/.test(versionResult.stdout); if (!hasValidVersion) { - console.log(`\n[verify] supabase --version FAILED:`); - console.log(` exit=${versionResult.exitCode}`); - console.log(` stdout=${JSON.stringify(versionResult.stdout)}`); - console.log(` stderr=${JSON.stringify(versionResult.stderr)}`); + yield* Console.log(`\n[verify] supabase --version FAILED:`); + yield* Console.log(` exit=${versionResult.exitCode}`); + yield* Console.log(` stdout=${yield* jsonQuote(versionResult.stdout)}`); + yield* Console.log(` stderr=${yield* jsonQuote(versionResult.stderr)}`); // Isolate "shim broken" from "platform binary broken". - const platformBin = await findPlatformBinary(testDir, ext); + const platformBin = yield* findPlatformBinary(testDir, ext); if (platformBin) { - console.log(`\n[verify] retrying via platform binary: ${platformBin}`); - const direct = await runCli(platformBin, ["--version"]); - console.log(` exit=${direct.exitCode}`); - console.log(` stdout=${JSON.stringify(direct.stdout)}`); - console.log(` stderr=${JSON.stringify(direct.stderr)}`); + yield* Console.log(`\n[verify] retrying via platform binary: ${platformBin}`); + const direct = yield* runCliEffect(platformBin, ["--version"]); + yield* Console.log(` exit=${direct.exitCode}`); + yield* Console.log(` stdout=${yield* jsonQuote(direct.stdout)}`); + yield* Console.log(` stderr=${yield* jsonQuote(direct.stderr)}`); } else { - console.log(`\n[verify] no platform binary found under node_modules/@supabase/*/bin/`); + yield* Console.log(`\n[verify] no platform binary found under node_modules/@supabase/*/bin/`); } } - const shellCheck = await verifyExpectedShell(binPath); + const shellCheck = yield* verifyExpectedShellEffect(binPath); const passed = hasValidVersion && shellCheck.passed; - console.log( - `\n${passed ? "PASS" : "FAIL"} — supabase --version exit=${versionResult.exitCode} stdout=${JSON.stringify(versionResult.stdout)}`, + yield* Console.log( + `\n${passed ? "PASS" : "FAIL"} — supabase --version exit=${versionResult.exitCode} stdout=${yield* jsonQuote(versionResult.stdout)}`, ); - console.log(shellCheck.detail); + yield* Console.log(shellCheck.detail); return passed; +}); + +export function runNpmTest(version: string, tag = "latest"): Promise { + return runRegistry(runNpmTestEffect(version, tag)); } diff --git a/apps/cli/tests/helpers/release-fixture.ts b/apps/cli/tests/helpers/release-fixture.ts index fe89bedeea..a7598fb289 100644 --- a/apps/cli/tests/helpers/release-fixture.ts +++ b/apps/cli/tests/helpers/release-fixture.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; // oxlint-disable-next-line effecttsgo/node-builtin-import -- the fixture owns a local native listener. import { createServer } from "node:http"; -import { Effect } from "effect"; +import { Effect, Schema } from "effect"; const TARGETS = ["darwin-arm64", "linux-amd64", "linux-arm64"] as const; @@ -20,13 +20,15 @@ export interface ReleaseFixture { readonly requests: Array; } +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + const sha256 = (text: string | Uint8Array) => createHash("sha256").update(text).digest("hex"); /** Serves `/releases/…`, `/token` and `/v2/…` from OS-assigned loopback port until the scope closes. */ export const startReleaseFixture = Effect.acquireRelease( - Effect.callback<{ close: () => Promise; fixture: ReleaseFixture }>((resume) => { + Effect.gen(function* () { const routes = new Map([ - ["/token", { body: JSON.stringify({ token: "anon" }) }], + ["/token", { body: yield* encodeJson({ token: "anon" }) }], ]); const failing = new Map(); const requests: Array = []; @@ -44,22 +46,29 @@ export const startReleaseFixture = Effect.acquireRelease( response.setHeader(name, value); response.end(served?.body ?? ""); }); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (address === null || typeof address === "string") return; - resume( - Effect.succeed({ - close: () => - new Promise((done) => { - server.close(() => done()); - server.closeAllConnections(); - }), - fixture: { origin: `http://127.0.0.1:${address.port}`, routes, failing, requests }, - }), - ); + const port = yield* Effect.callback((resume) => { + const onError = (error: Error) => resume(Effect.die(error)); + server.once("error", onError); + server.listen(0, "127.0.0.1", () => { + server.off("error", onError); + const address = server.address(); + if (address === null || typeof address === "string") return; + resume(Effect.succeed(address.port)); + }); }); + const fixture: ReleaseFixture = { + origin: `http://127.0.0.1:${port}`, + routes, + failing, + requests, + }; + return { server, fixture }; }), - ({ close }) => Effect.promise(close), + ({ server }) => + Effect.callback((resume) => { + server.close(() => resume(Effect.void)); + server.closeAllConnections(); + }), ).pipe(Effect.map(({ fixture }) => fixture)); /** The env that points the stack's release resolver at `fixture`. */ diff --git a/apps/cli/tests/helpers/release-shell.ts b/apps/cli/tests/helpers/release-shell.ts index a45798b3ad..14985c4481 100644 --- a/apps/cli/tests/helpers/release-shell.ts +++ b/apps/cli/tests/helpers/release-shell.ts @@ -1,41 +1,60 @@ -type ShellCheckResult = { - readonly passed: boolean; - readonly detail: string; -}; - -export interface CliRunResult { - readonly stdout: string; - readonly stderr: string; - readonly exitCode: number; -} - -export async function runCli(binPath: string, args: Array): Promise { - const proc = Bun.spawn([binPath, ...args], { - stdout: "pipe", - stderr: "pipe", - }); - - const [stdout, stderr, exitCode] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - proc.exited, - ]); +import { Data, Effect, Exit, Schema } from "effect"; + +export class ReleaseStepError extends Data.TaggedError("ReleaseStepError")<{ + readonly cause: unknown; +}> {} + +export const releaseStep = (run: () => PromiseLike) => + Effect.tryPromise({ try: run, catch: (cause) => new ReleaseStepError({ cause }) }); + +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + +/** `JSON.stringify` of captured output; encoding a string cannot fail. */ +export const jsonQuote = (text: string) => encodeJson(text).pipe(Effect.orDie); + +export const runCliEffect = Effect.fn("ReleaseShell.runCli")(function* ( + binPath: string, + args: Array, +) { + /* The scope owns the child: an interrupted run kills it and waits for it to exit. */ + const proc = yield* Effect.acquireRelease( + Effect.try({ + try: () => + Bun.spawn([binPath, ...args], { + stdout: "pipe", + stderr: "pipe", + }), + catch: (cause) => new ReleaseStepError({ cause }), + }), + (spawned, exit) => + Exit.hasInterrupts(exit) + ? Effect.sync(() => spawned.kill("SIGKILL")).pipe( + Effect.andThen(Effect.promise(() => spawned.exited)), + ) + : Effect.void, + ); + + const [stdout, stderr, exitCode] = yield* releaseStep(() => + Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]), + ); return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode, }; -} +}, Effect.scoped); -export async function verifyExpectedShell(binPath: string): Promise { - const result = await runCli(binPath, ["init", "--help"]); +export const verifyExpectedShellEffect = Effect.fn("ReleaseShell.verifyExpectedShell")(function* ( + binPath: string, +) { + const result = yield* runCliEffect(binPath, ["init", "--help"]); const output = [result.stdout, result.stderr].filter(Boolean).join("\n"); const passed = result.exitCode === 0 && output.includes("init"); return { passed, detail: passed ? 'dispatch ok: "init --help" succeeded' - : `expected dispatch via "init --help", got exit=${result.exitCode}, stdout=${JSON.stringify(result.stdout)}, stderr=${JSON.stringify(result.stderr)}`, + : `expected dispatch via "init --help", got exit=${result.exitCode}, stdout=${yield* jsonQuote(result.stdout)}, stderr=${yield* jsonQuote(result.stderr)}`, }; -} +}); diff --git a/apps/cli/tests/helpers/stack-cleanup.integration.test.ts b/apps/cli/tests/helpers/stack-cleanup.integration.test.ts index 57430d8b08..99740cfddd 100644 --- a/apps/cli/tests/helpers/stack-cleanup.integration.test.ts +++ b/apps/cli/tests/helpers/stack-cleanup.integration.test.ts @@ -27,7 +27,7 @@ describe("test stack cleanup", () => { }); const result = yield* Effect.exit( Effect.acquireUseRelease( - Effect.succeed(undefined), + Effect.void, () => Effect.gen(function* () { yield* stack.services.create({ diff --git a/apps/cli/tests/helpers/stack-cleanup.ts b/apps/cli/tests/helpers/stack-cleanup.ts index c5326195a5..5a2aaead59 100644 --- a/apps/cli/tests/helpers/stack-cleanup.ts +++ b/apps/cli/tests/helpers/stack-cleanup.ts @@ -27,6 +27,6 @@ export const destroyTestStacks = ( const combined = failures .slice(1) .reduce((cause, exit) => Cause.combine(cause, exit.cause), first.cause); - yield* Effect.failCause(combined); + return yield* Effect.failCause(combined); } }).pipe(Effect.orDie); diff --git a/apps/cli/tests/helpers/stack-e2e-cleanup.ts b/apps/cli/tests/helpers/stack-e2e-cleanup.ts index 24854055a6..7b546bbea2 100644 --- a/apps/cli/tests/helpers/stack-e2e-cleanup.ts +++ b/apps/cli/tests/helpers/stack-e2e-cleanup.ts @@ -1,11 +1,27 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs"; -import path from "node:path"; +import { BunChildProcessSpawner, BunFileSystem, BunPath } from "@effect/platform-bun"; +import { + Cause, + Clock, + Console, + Data, + Effect, + Fiber, + FileSystem, + Layer, + Option, + Path, + Predicate, + Schedule, + Schema, + Stream, +} from "effect"; +import type { PlatformError } from "effect/PlatformError"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; import { runSupabase } from "./cli.ts"; type TempHome = { readonly dir: string; - readonly dispose: () => void; + readonly dispose: () => void | Promise; }; type StackProject = { @@ -21,9 +37,14 @@ interface StackRuntimeSnapshot { readonly trackedPids: ReadonlyArray; } +type MaybePromise = A | PromiseLike; + interface CleanupEnvironment { readonly stopStack: (projectDir: string, homeDir: string) => Promise<{ exitCode: number }>; - readonly captureSnapshot: (projectDir: string, homeDir?: string) => StackRuntimeSnapshot; + readonly captureSnapshot: ( + projectDir: string, + homeDir?: string, + ) => MaybePromise; readonly waitForCleanup: ( projectDir: string, homeDir: string | undefined, @@ -35,8 +56,8 @@ interface CleanupEnvironment { snapshot: StackRuntimeSnapshot, ) => Promise; readonly removeProjectWithDocker: (projectDir: string) => Promise; - readonly repairProjectPermissions: (projectDir: string) => void; - readonly describeProjectPermissions: (projectDir: string) => string; + readonly repairProjectPermissions: (projectDir: string) => MaybePromise; + readonly describeProjectPermissions: (projectDir: string) => MaybePromise; } interface StackE2eCleanupManager { @@ -50,14 +71,74 @@ interface StackE2eCleanupManager { reset(): void; } -function normalizeDir(dir: string): string { +type HostServices = FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner; + +const hostLayer: Layer.Layer = BunChildProcessSpawner.layer.pipe( + Layer.provideMerge(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), +); + +/* Promise edge of the real environment: a host failure rejects with the original error. */ +const runHost = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe( + Effect.catch((error) => Effect.die(error.cause ?? error)), + Effect.provide(hostLayer), + ), + ); + +/** A foreign cleanup callback or environment member rejected or threw. */ +class CleanupForeignFailure extends Data.TaggedError("CleanupForeignFailure")<{ + readonly cause: unknown; +}> {} + +/** An owned path could not be removed; `detail` is the reported reason. */ +class OwnedPathCleanupFailure extends Data.TaggedError("OwnedPathCleanupFailure")<{ + readonly detail: string; +}> {} + +const callForeign = (run: () => MaybePromise) => + Effect.tryPromise({ + try: () => Promise.resolve(run()), + catch: (cause) => new CleanupForeignFailure({ cause }), + }); + +const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error)); + +const foreignMessage = (error: PlatformError) => errorMessage(error.cause ?? error); + +const normalizeDir = Effect.fnUntraced(function* (dir: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const resolved = path.resolve(dir); - try { - return realpathSync.native(resolved); - } catch { - return resolved; + return yield* fs.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)); +}); + +const pathExists = Effect.fnUntraced(function* (pathname: string) { + const fs = yield* FileSystem.FileSystem; + return yield* fs.exists(pathname).pipe(Effect.orElseSucceed(() => false)); +}); + +/* + * Directory entries never follow symlinks. `FileSystem` has no `lstat`, so `readLink` decides: only + * its "not a symlink" answer (`EINVAL`) lets `stat` judge the entry; a link, or an entry `readLink` + * is denied on (a mode-000 link on macOS), is never walked. + */ +const isDirectoryEntry = Effect.fnUntraced(function* (pathname: string) { + const fs = yield* FileSystem.FileSystem; + const notASymlink = yield* fs.readLink(pathname).pipe( + Effect.as(false), + Effect.catchTag("PlatformError", (error) => + Effect.succeed(Predicate.hasProperty(error.cause, "code") && error.cause.code === "EINVAL"), + ), + ); + if (!notASymlink) { + return false; } -} + return yield* fs.stat(pathname).pipe( + Effect.map((info) => info.type === "Directory"), + Effect.orElseSucceed(() => false), + ); +}); function isProcessAlive(pid: number): boolean { try { @@ -68,48 +149,121 @@ function isProcessAlive(pid: number): boolean { } } -function parsePsTable(): Array<{ +function killQuietly(pid: number): void { + try { + process.kill(pid, "SIGKILL"); + } catch {} +} + +function parsePsOutput(output: string): Array<{ readonly pid: number; readonly ppid: number; readonly command: string; }> { - try { - const output = execFileSync("ps", ["-Ao", "pid=,ppid=,command="], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - - return output - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0) - .map((line) => { - const match = line.match(/^(\d+)\s+(\d+)\s+(.*)$/); - if (match == null) { - return undefined; - } - return { - pid: Number.parseInt(match[1]!, 10), - ppid: Number.parseInt(match[2]!, 10), - command: match[3] ?? "", - }; - }) - .filter( - ( - entry, - ): entry is { readonly pid: number; readonly ppid: number; readonly command: string } => - entry != null, - ); - } catch { - return []; - } + return output + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => { + const match = line.match(/^(\d+)\s+(\d+)\s+(.*)$/); + if (match == null) { + return undefined; + } + return { + pid: Number.parseInt(match[1]!, 10), + ppid: Number.parseInt(match[2]!, 10), + command: match[3] ?? "", + }; + }) + .filter( + (entry): entry is { readonly pid: number; readonly ppid: number; readonly command: string } => + entry != null, + ); } +/** The output limit of a synchronous spawn: its default 1 MiB `maxBuffer`. */ +const SYNC_SPAWN_MAX_BUFFER = 1024 * 1024; + +/** The most a synchronous spawn reads from a pipe at once. */ +const SYNC_SPAWN_READ_SIZE = 64 * 1024; + +/** The output outgrew the synchronous-spawn buffer limit. */ +class OutputBufferExceeded extends Data.TaggedError("OutputBufferExceeded")<{ + readonly bytes: Uint8Array; +}> {} + +const concatBytes = (chunks: ReadonlyArray, size: number) => { + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return bytes; +}; + +/** + * Buffers a stream the way a synchronous spawn does: once the total passes the limit it stops + * with what was read so far, the chunk that crossed the limit included. + */ +const collectBounded = (stream: Stream.Stream) => + Effect.suspend(() => { + const chunks: Array = []; + let size = 0; + return Stream.runForEach(stream, (chunk) => + Effect.suspend(() => { + chunks.push(chunk); + size += chunk.byteLength; + return size > SYNC_SPAWN_MAX_BUFFER + ? Effect.fail(new OutputBufferExceeded({ bytes: concatBytes(chunks, size) })) + : Effect.void; + }), + ).pipe(Effect.map(() => concatBytes(chunks, size))); + }); + +/* Recovers every failure a spawn can raise, its throws included; interruption passes through. */ +const recoverUnlessInterrupted = ( + effect: Effect.Effect, + recover: (failure: unknown) => B, +): Effect.Effect => + effect.pipe( + Effect.catchCause((cause) => + Cause.hasInterruptsOnly(cause) + ? Effect.failCause(Cause.fromReasons(cause.reasons.filter(Cause.isInterruptReason))) + : Effect.succeed(recover(Cause.squash(cause))), + ), + ); + +const parsePsTable = recoverUnlessInterrupted( + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const output = yield* Effect.scoped( + Effect.gen(function* () { + const handle = yield* spawner.spawn( + ChildProcess.make("ps", ["-Ao", "pid=,ppid=,command="], { + /* A plain child, not a process group: no process-group signals. */ + detached: false, + stdin: "ignore", + stderr: "ignore", + }), + ); + const [bytes, exitCode] = yield* Effect.all( + [collectBounded(handle.stdout), handle.exitCode], + { concurrency: "unbounded" }, + ); + return exitCode === 0 ? Option.some(utf8.decode(bytes)) : Option.none(); + }), + ); + return Option.match(output, { onNone: () => [], onSome: parsePsOutput }); + }), + () => [], +); + function descendantPids( - rootPids: ReadonlyArray, + rootPids: ReadonlyArray, table: ReadonlyArray<{ readonly pid: number; readonly ppid: number }>, -): Array { - const visited = new Set(); +): Array { + const visited = new Set(); const pending = [...rootPids]; while (pending.length > 0) { @@ -128,25 +282,45 @@ function descendantPids( return [...visited]; } -function readDocumentPid(documentFile: string): number | undefined { - try { - const parsed = JSON.parse(readFileSync(documentFile, "utf8")) as { - readonly runtime?: { readonly pid?: number }; - }; - return parsed.runtime?.pid; - } catch { - return undefined; - } -} - -function cleanupErrorDetail( - pathname: string, - error: unknown, - resource = "temp stack project", -): string { - return `Failed to remove ${resource} ${pathname}: ${ - error instanceof Error ? error.message : String(error) - }`; +const decodeStackWorkspace = Schema.decodeUnknownEffect( + Schema.fromJsonString( + Schema.Struct({ + workspace: Schema.optional(Schema.Struct({ path: Schema.optional(Schema.String) })), + }), + ), +); + +const decodeJsonDocument = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); + +/* Reads UTF-8 text and keeps a leading byte order mark, like `readFileSync(path, "utf8")`. */ +const utf8 = new TextDecoder("utf-8", { ignoreBOM: true }); +const readUtf8 = (fs: FileSystem.FileSystem, pathname: string) => + fs.readFile(pathname).pipe(Effect.map((bytes) => utf8.decode(bytes))); + +const readWorkspacePath = Effect.fnUntraced(function* (documentFile: string) { + const fs = yield* FileSystem.FileSystem; + return yield* readUtf8(fs, documentFile).pipe( + Effect.flatMap(decodeStackWorkspace), + Effect.map((document) => document.workspace?.path), + Effect.orElseSucceed(() => undefined), + ); +}); + +const readDocumentPid = Effect.fnUntraced(function* (documentFile: string) { + const fs = yield* FileSystem.FileSystem; + return yield* readUtf8(fs, documentFile).pipe( + Effect.flatMap(decodeJsonDocument), + /* `runtime.pid` as written, whatever its JSON type, like a plain property read. */ + Effect.map((document) => { + const runtime = Predicate.hasProperty(document, "runtime") ? document.runtime : undefined; + return Predicate.hasProperty(runtime, "pid") ? runtime.pid : undefined; + }), + Effect.orElseSucceed(() => undefined), + ); +}); + +function cleanupErrorDetail(pathname: string, detail: string, resource = "temp stack project") { + return `Failed to remove ${resource} ${pathname}: ${detail}`; } // Walks the `cause` chain so a typed wrapper classifies by the errno it carries. @@ -166,169 +340,228 @@ function formatMode(mode: number): string { return `0${(mode & 0o777).toString(8)}`; } -function describePath(pathname: string): string { - try { - const stats = statSync(pathname); - return `${pathname} uid=${stats.uid} gid=${stats.gid} mode=${formatMode(stats.mode)}`; - } catch (error) { - return `${pathname} ${error instanceof Error ? error.message : String(error)}`; - } -} - -function describeProjectPermissions(projectDir: string): string { +const describePath = Effect.fnUntraced(function* (pathname: string) { + const fs = yield* FileSystem.FileSystem; + return yield* fs.stat(pathname).pipe( + Effect.map( + (stats) => + `${pathname} uid=${Option.getOrUndefined(stats.uid)} gid=${Option.getOrUndefined(stats.gid)} mode=${formatMode(stats.mode)}`, + ), + Effect.catch((error) => Effect.succeed(`${pathname} ${foreignMessage(error)}`)), + ); +}); + +const describeProjectPermissions = Effect.fnUntraced(function* (projectDir: string) { + const path = yield* Path.Path; const details = [ "Permission diagnostics:", - describePath(projectDir), - describePath(path.join(projectDir, ".supabase")), + yield* describePath(projectDir), + yield* describePath(path.join(projectDir, ".supabase")), ]; return details.join("\n"); -} - -function repairProjectPermissions(projectDir: string): void { - try { - execFileSync("chmod", ["-R", "u+rwX", projectDir], { - stdio: "ignore", - timeout: 5_000, - }); - } catch {} -} - -async function removeProjectWithDocker(projectDir: string): Promise { - const parentDir = path.dirname(projectDir); - const projectName = path.basename(projectDir); - - let dockerErr: string | undefined; - try { - execFileSync( - "docker", - [ - "run", - "--rm", - "--user", - "0:0", - "-v", - `${parentDir}:/parent`, - "-e", - `TARGET_NAME=${projectName}`, - "--entrypoint", - "sh", - "public.ecr.aws/docker/library/busybox:1.36", - "-c", - 'cd /parent && rm -rf -- "$TARGET_NAME"', - ], - { stdio: ["ignore", "ignore", "pipe"], timeout: 30_000 }, - ); - } catch (error) { - const rawStderr = - error != null && typeof error === "object" && "stderr" in error - ? (error as { stderr: unknown }).stderr - : ""; - const stderr = ( - typeof rawStderr === "string" - ? rawStderr - : rawStderr instanceof Uint8Array - ? new TextDecoder().decode(rawStderr) - : "" - ).trim(); - dockerErr = stderr || (error instanceof Error ? error.message : String(error)); - } +}); + +const repairProjectPermissions = Effect.fnUntraced(function* (projectDir: string) { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + yield* spawner + .exitCode( + ChildProcess.make("chmod", ["-R", "u+rwX", projectDir], { + detached: false, + stdin: "ignore", + stdout: "ignore", + stderr: "ignore", + }), + ) + .pipe(Effect.timeoutOption(5_000), (run) => recoverUnlessInterrupted(run, () => undefined)); +}); + +const removeProjectWithDocker = Effect.fnUntraced(function* (projectDir: string) { + const path = yield* Path.Path; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const args = [ + "run", + "--rm", + "--user", + "0:0", + "-v", + `${path.dirname(projectDir)}:/parent`, + "-e", + `TARGET_NAME=${path.basename(projectDir)}`, + "--entrypoint", + "sh", + "public.ecr.aws/docker/library/busybox:1.36", + "-c", + 'cd /parent && rm -rf -- "$TARGET_NAME"', + ]; - const removed = !existsSync(projectDir); + /* The stderr read so far, kept across a timeout or an outgrown buffer for the report. */ + const stderrChunks: Array = []; + let stderrSize = 0; + const failure = yield* recoverUnlessInterrupted( + Effect.scoped( + Effect.gen(function* () { + const handle = yield* spawner.spawn( + ChildProcess.make("docker", args, { + /* A plain child, not a process group: its descendants are left alone. */ + detached: false, + stdin: "ignore", + stdout: "ignore", + stderr: "pipe", + }), + ); + const [, exitCode] = yield* Effect.all( + [ + Stream.runForEach(handle.stderr, (chunk) => + Effect.suspend(() => { + stderrChunks.push(chunk); + stderrSize += chunk.byteLength; + return stderrSize > SYNC_SPAWN_MAX_BUFFER + ? Effect.fail(new OutputBufferExceeded({ bytes: new Uint8Array() })) + : Effect.void; + }), + ), + /* A signal death fails like a nonzero exit. */ + handle.exitCode.pipe(Effect.orElseSucceed(() => -1)), + ], + { concurrency: "unbounded" }, + ); + return exitCode; + }), + ).pipe( + Effect.timeoutOption(30_000), + Effect.map( + Option.match({ + onNone: () => "spawnSync docker ETIMEDOUT", + onSome: (exitCode) => + exitCode === 0 ? undefined : `Command failed: docker ${args.join(" ")}`, + }), + ), + Effect.catchTag("OutputBufferExceeded", () => + Effect.succeed( + "spawnSync docker ENOBUFS (stdout or stderr buffer reached maxBuffer size limit)", + ), + ), + ), + (thrown) => + errorMessage( + Predicate.isTagged(thrown, "PlatformError") && Predicate.hasProperty(thrown, "cause") + ? thrown.cause + : thrown, + ), + ); + /* A synchronous spawn reads in 64 KiB pieces, so an outgrown buffer keeps through the piece that + crossed the limit. */ + const keptSize = Math.min(stderrSize, SYNC_SPAWN_MAX_BUFFER + SYNC_SPAWN_READ_SIZE); + const stderr = new TextDecoder().decode( + concatBytes(stderrChunks, stderrSize).subarray(0, keptSize), + ); + const dockerErr = failure === undefined ? undefined : stderr.trim() || failure; + + const removed = !(yield* pathExists(projectDir)); if (!removed && dockerErr) { - console.warn(`[stack-e2e-cleanup] docker fallback for ${projectDir} failed: ${dockerErr}`); + yield* Console.warn( + `[stack-e2e-cleanup] docker fallback for ${projectDir} failed: ${dockerErr}`, + ); } return removed; -} +}); -async function cleanupOwnedPath( - pathname: string, - cleanup: () => void | Promise, - environment: Pick< - CleanupEnvironment, - "removeProjectWithDocker" | "repairProjectPermissions" | "describeProjectPermissions" - >, -): Promise { - try { - await cleanup(); - } catch (error) { - if (!isPermissionError(error)) { - throw error; - } +type PermissionFallback = Pick< + CleanupEnvironment, + "removeProjectWithDocker" | "repairProjectPermissions" | "describeProjectPermissions" +>; - const removedByDocker = await environment.removeProjectWithDocker(pathname); - if (removedByDocker) { - return; - } +const reportedAs = (run: () => MaybePromise) => + callForeign(run).pipe( + Effect.mapError( + (failure) => new OwnedPathCleanupFailure({ detail: errorMessage(failure.cause) }), + ), + ); - environment.repairProjectPermissions(pathname); - try { - await cleanup(); - } catch (retryError) { - throw new Error( - `${retryError instanceof Error ? retryError.message : String(retryError)}\n${environment.describeProjectPermissions( - pathname, - )}`, - ); - } +const recoverPermissionBlockedPath = Effect.fnUntraced(function* ( + pathname: string, + cleanup: () => void | Promise, + environment: PermissionFallback, +) { + const removedByDocker = yield* reportedAs(() => environment.removeProjectWithDocker(pathname)); + if (removedByDocker) { + return; } -} - -async function cleanupProject( - project: StackProject, - environment: Pick< - CleanupEnvironment, - "removeProjectWithDocker" | "repairProjectPermissions" | "describeProjectPermissions" - >, -): Promise { - await cleanupOwnedPath(project.dir, project.cleanup, environment); -} -function captureSnapshot(projectDir: string, homeDir?: string): StackRuntimeSnapshot { - const normalized = normalizeDir(projectDir); + yield* reportedAs(() => environment.repairProjectPermissions(pathname)); + yield* callForeign(cleanup).pipe( + Effect.catch((retryFailure) => + reportedAs(() => environment.describeProjectPermissions(pathname)).pipe( + Effect.flatMap((diagnostics) => + Effect.fail( + new OwnedPathCleanupFailure({ + detail: `${errorMessage(retryFailure.cause)}\n${diagnostics}`, + }), + ), + ), + ), + ), + ); +}); + +const cleanupOwnedPath = ( + pathname: string, + cleanup: () => void | Promise, + environment: PermissionFallback, +) => + callForeign(cleanup).pipe( + Effect.catch((failure) => + isPermissionError(failure.cause) + ? recoverPermissionBlockedPath(pathname, cleanup, environment) + : Effect.fail(new OwnedPathCleanupFailure({ detail: errorMessage(failure.cause) })), + ), + ); + +const captureSnapshot = Effect.fnUntraced(function* (projectDir: string, homeDir?: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const normalized = yield* normalizeDir(projectDir); const managedStacksRoot = - homeDir === undefined ? undefined : path.join(normalizeDir(homeDir), "stacks"); - if (managedStacksRoot === undefined || !existsSync(managedStacksRoot)) { + homeDir === undefined ? undefined : path.join(yield* normalizeDir(homeDir), "stacks"); + if (managedStacksRoot === undefined || !(yield* pathExists(managedStacksRoot))) { return { managedStacksRootExists: false, documentFiles: [], stackDirs: [], trackedPids: [], - }; + } satisfies StackRuntimeSnapshot; } const stackDirs: Array = []; const documentFiles: Array = []; - for (const entry of readdirSync(managedStacksRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) { + for (const entry of yield* fs.readDirectory(managedStacksRoot)) { + const stackDir = path.join(managedStacksRoot, entry); + if (!(yield* isDirectoryEntry(stackDir))) { continue; } - const stackDir = path.join(managedStacksRoot, entry.name); const documentFile = path.join(stackDir, "stack.json"); - if (!existsSync(documentFile)) { + if (!(yield* pathExists(documentFile))) { continue; } - try { - const document = JSON.parse(readFileSync(documentFile, "utf8")) as { - readonly workspace?: { readonly path?: string }; - }; - if ( - document.workspace?.path === undefined || - normalizeDir(document.workspace.path) !== normalized - ) { - continue; - } - } catch { + const workspacePath = yield* readWorkspacePath(documentFile); + if (workspacePath === undefined || (yield* normalizeDir(workspacePath)) !== normalized) { continue; } stackDirs.push(stackDir); documentFiles.push(documentFile); } - const rootPids = documentFiles - .map(readDocumentPid) - .filter((pid): pid is number => pid != null && pid > 0); - const table = parsePsTable(); + /* A pid is kept as written when it compares above zero; only a numeric one matches a `ps` row, + and process signals coerce the others to numbers. */ + const rootPids: Array = []; + for (const documentFile of documentFiles) { + const pid = yield* readDocumentPid(documentFile); + if (pid != null && Number(pid) > 0) { + rootPids.push(pid); + } + } + const table = yield* parsePsTable; const descendants = descendantPids(rootPids, table); const commandPids = table.filter((row) => row.command.includes(normalized)).map((row) => row.pid); @@ -336,165 +569,236 @@ function captureSnapshot(projectDir: string, homeDir?: string): StackRuntimeSnap managedStacksRootExists: stackDirs.length > 0, documentFiles, stackDirs, - trackedPids: [...new Set([...descendants, ...commandPids])].sort((left, right) => left - right), - }; -} + trackedPids: [...new Set([...descendants, ...commandPids])] + .sort((left, right) => Number(left) - Number(right)) + .map(Number), + } satisfies StackRuntimeSnapshot; +}); -async function waitForCleanup( - projectDir: string, - homeDir: string | undefined, - snapshot: StackRuntimeSnapshot, -): Promise { - const deadline = Date.now() + 15_000; - while (Date.now() < deadline) { - const nextSnapshot = captureSnapshot(projectDir, homeDir); - const filesGone = nextSnapshot.documentFiles.length === 0; - const pidsGone = snapshot.trackedPids.every((pid) => !isProcessAlive(pid)); - if (filesGone && pidsGone) { - return true; - } - await new Promise((resolve) => setTimeout(resolve, 200)); - } - - return false; -} +type CleanupRound = "clean" | "pending" | "expired"; -async function forceCleanup( +const waitForCleanup = Effect.fnUntraced(function* ( projectDir: string, homeDir: string | undefined, snapshot: StackRuntimeSnapshot, -): Promise { +) { + const deadline = (yield* Clock.currentTimeMillis) + 15_000; + /* Each round checks the deadline before it looks; a round that still sees the stack waits 200 ms. */ + const round = Effect.flatMap(Clock.currentTimeMillis, (now) => + now < deadline + ? Effect.map(captureSnapshot(projectDir, homeDir), (nextSnapshot): CleanupRound => { + const filesGone = nextSnapshot.documentFiles.length === 0; + const pidsGone = snapshot.trackedPids.every((pid) => !isProcessAlive(pid)); + return filesGone && pidsGone ? "clean" : "pending"; + }) + : Effect.succeed("expired"), + ); + const state = yield* round.pipe( + Effect.repeat({ schedule: Schedule.spaced(200), while: (current) => current === "pending" }), + ); + return state === "clean"; +}); + +const forceCleanup = Effect.fnUntraced(function* (snapshot: StackRuntimeSnapshot) { + const fs = yield* FileSystem.FileSystem; for (const pid of snapshot.trackedPids) { - try { - process.kill(pid, "SIGKILL"); - } catch {} + killQuietly(pid); } for (const stackDir of snapshot.stackDirs) { - try { - rmSync(stackDir, { recursive: true, force: true }); - } catch {} + yield* fs.remove(stackDir, { recursive: true, force: true }).pipe(Effect.ignore); } -} +}); function createRealEnvironment(): CleanupEnvironment { return { - stopStack: async (projectDir, homeDir) => { - const result = await runSupabase(["stack", "destroy", "--yes"], { + stopStack: (projectDir, homeDir) => + runSupabase(["stack", "destroy", "--yes"], { cwd: projectDir, home: homeDir, env: { SUPABASE_EXPERIMENTAL_STACK: "1" }, exitTimeoutMs: 15_000, - }); - return { exitCode: result.exitCode }; - }, - captureSnapshot, - waitForCleanup, - forceCleanup, - removeProjectWithDocker, - repairProjectPermissions, - describeProjectPermissions, + }).then((result) => ({ exitCode: result.exitCode })), + captureSnapshot: (projectDir, homeDir) => runHost(captureSnapshot(projectDir, homeDir)), + waitForCleanup: (projectDir, homeDir, snapshot) => + runHost(waitForCleanup(projectDir, homeDir, snapshot)), + forceCleanup: (_projectDir, _homeDir, snapshot) => runHost(forceCleanup(snapshot)), + removeProjectWithDocker: (projectDir) => runHost(removeProjectWithDocker(projectDir)), + repairProjectPermissions: (projectDir) => runHost(repairProjectPermissions(projectDir)), + describeProjectPermissions: (projectDir) => runHost(describeProjectPermissions(projectDir)), }; } +type Registration = + | { + readonly kind: "home"; + readonly dir: Fiber.Fiber; + readonly dispose: () => void | Promise; + } + | { + readonly kind: "project"; + readonly dir: Fiber.Fiber; + readonly cleanup: () => Promise; + } + | { + readonly kind: "association"; + readonly projectDir: Fiber.Fiber; + readonly homeDir: Fiber.Fiber; + }; + +/* Registration stays synchronous, so canonicalization starts now, while the owned path exists. */ +const startNormalizeDir = (dir: string): Fiber.Fiber => + Effect.runFork(normalizeDir(dir).pipe(Effect.provide(hostLayer))); + +/* Replays registrations in order into maps keyed by canonical dir. */ +const resolveRegistrations = Effect.fnUntraced(function* ( + registrations: ReadonlyArray, +) { + const homes = new Map(); + const projects = new Map(); + for (const registration of registrations) { + switch (registration.kind) { + case "home": { + const dir = yield* Fiber.join(registration.dir); + homes.set(dir, { dir, dispose: registration.dispose }); + break; + } + case "project": { + const dir = yield* Fiber.join(registration.dir); + projects.set(dir, { dir, cleanup: registration.cleanup }); + break; + } + case "association": { + const project = projects.get(yield* Fiber.join(registration.projectDir)); + if (project !== undefined) { + project.homeDir = yield* Fiber.join(registration.homeDir); + } + break; + } + } + } + return { pendingProjects: [...projects.values()], pendingHomes: homes }; +}); + +const drainRegistrations = Effect.fnUntraced(function* ( + environment: CleanupEnvironment, + registrations: ReadonlyArray, +) { + const { pendingProjects, pendingHomes } = yield* resolveRegistrations(registrations); + + const failures: Array = []; + const associatedHomes = new Map(); + + for (const project of pendingProjects) { + const homeDir = project.homeDir; + const home = homeDir === undefined ? undefined : pendingHomes.get(homeDir); + if (home !== undefined && homeDir !== undefined) { + associatedHomes.set(homeDir, home); + } + const snapshot = yield* callForeign(() => + environment.captureSnapshot(project.dir, project.homeDir), + ); + const hasRuntimeArtifacts = + snapshot.documentFiles.length > 0 || snapshot.trackedPids.some((pid) => isProcessAlive(pid)); + const hasStackPersistence = snapshot.managedStacksRootExists || snapshot.stackDirs.length > 0; + let stopExitCode: number | undefined; + + const stackHomeDir = project.homeDir; + if (hasStackPersistence && stackHomeDir !== undefined) { + stopExitCode = yield* callForeign(() => + environment.stopStack(project.dir, stackHomeDir), + ).pipe( + Effect.map((stopResult) => stopResult.exitCode), + Effect.orElseSucceed(() => 1), + ); + } + + if (hasRuntimeArtifacts) { + const cleaned = yield* callForeign(() => + environment.waitForCleanup(project.dir, project.homeDir, snapshot), + ); + if (!cleaned) { + yield* callForeign(() => environment.forceCleanup(project.dir, project.homeDir, snapshot)); + failures.push( + stopExitCode != null && stopExitCode !== 0 + ? `Centralized e2e cleanup detected leaked stack resources for ${project.dir} after stop exited ${stopExitCode}.` + : `Centralized e2e cleanup detected leaked stack resources for ${project.dir}.`, + ); + } + } + + yield* cleanupOwnedPath(project.dir, project.cleanup, environment).pipe( + Effect.catch((failure) => + Effect.sync(() => { + failures.push(cleanupErrorDetail(project.dir, failure.detail)); + }), + ), + ); + } + + for (const home of associatedHomes.values()) { + yield* cleanupOwnedPath(home.dir, home.dispose, environment).pipe( + Effect.catch((failure) => + Effect.sync(() => { + failures.push(cleanupErrorDetail(home.dir, failure.detail, "temp home")); + }), + ), + ); + } + + // Cleanup failures are best-effort and don't fail the test: assertions already + // ran, CI runners are ephemeral, and cases like `functions dev`'s root-owned + // docker files can make cleanup itself impossible. + if (failures.length > 0) { + yield* Console.warn( + `[stack-e2e-cleanup] ${failures.length} resource(s) could not be cleaned up:\n${failures.join("\n")}`, + ); + } +}); + export function createStackE2eCleanupManager( environment: CleanupEnvironment = createRealEnvironment(), ): StackE2eCleanupManager { - const homes = new Map(); - const projects = new Map(); + let registrations: Array = []; return { registerHome(home) { - const dir = normalizeDir(home.dir); - homes.set(dir, { - dir, + registrations.push({ + kind: "home", + dir: startNormalizeDir(home.dir), dispose: () => home.dispose(), }); }, registerStackProject(project) { - const dir = normalizeDir(project.dir); - projects.set(dir, { - dir, + registrations.push({ + kind: "project", + dir: startNormalizeDir(project.dir), cleanup: () => project.cleanup(), }); }, associateHome(projectDir, homeDir) { - const project = projects.get(normalizeDir(projectDir)); - if (project !== undefined) { - project.homeDir = normalizeDir(homeDir); + /* With no stack project registered yet there is nothing to associate, so nothing is kept. */ + if (!registrations.some((registration) => registration.kind === "project")) { + return; } + registrations.push({ + kind: "association", + projectDir: startNormalizeDir(projectDir), + homeDir: startNormalizeDir(homeDir), + }); }, - async drain() { - const pendingProjects = [...projects.values()]; - const pendingHomes = new Map(homes); - projects.clear(); - homes.clear(); - - const failures: Array = []; - const associatedHomes = new Map(); - - for (const project of pendingProjects) { - const homeDir = project.homeDir; - const home = homeDir === undefined ? undefined : pendingHomes.get(homeDir); - if (home !== undefined && homeDir !== undefined) { - associatedHomes.set(homeDir, home); - } - const snapshot = environment.captureSnapshot(project.dir, project.homeDir); - const hasRuntimeArtifacts = - snapshot.documentFiles.length > 0 || - snapshot.trackedPids.some((pid) => isProcessAlive(pid)); - const hasStackPersistence = - snapshot.managedStacksRootExists || snapshot.stackDirs.length > 0; - let stopExitCode: number | undefined; - - if (hasStackPersistence && project.homeDir !== undefined) { - try { - const stopResult = await environment.stopStack(project.dir, project.homeDir); - stopExitCode = stopResult.exitCode; - } catch { - stopExitCode = 1; - } - } - - if (hasRuntimeArtifacts) { - const cleaned = await environment.waitForCleanup(project.dir, project.homeDir, snapshot); - if (!cleaned) { - await environment.forceCleanup(project.dir, project.homeDir, snapshot); - failures.push( - stopExitCode != null && stopExitCode !== 0 - ? `Centralized e2e cleanup detected leaked stack resources for ${project.dir} after stop exited ${stopExitCode}.` - : `Centralized e2e cleanup detected leaked stack resources for ${project.dir}.`, - ); - } - } - - try { - await cleanupProject(project, environment); - } catch (error) { - failures.push(cleanupErrorDetail(project.dir, error)); - } - } - - for (const home of associatedHomes.values()) { - try { - await cleanupOwnedPath(home.dir, home.dispose, environment); - } catch (error) { - failures.push(cleanupErrorDetail(home.dir, error, "temp home")); - } - } - - // Cleanup failures are best-effort and don't fail the test: assertions already - // ran, CI runners are ephemeral, and cases like `functions dev`'s root-owned - // docker files can make cleanup itself impossible. - if (failures.length > 0) { - console.warn( - `[stack-e2e-cleanup] ${failures.length} resource(s) could not be cleaned up:\n${failures.join("\n")}`, - ); - } + drain() { + const pending = registrations; + registrations = []; + /* A foreign rejection rejects the drain with the original value. */ + return Effect.runPromise( + drainRegistrations(environment, pending).pipe( + Effect.catch((failure) => Effect.die(failure.cause)), + ), + ); }, reset() { - homes.clear(); - projects.clear(); + registrations = []; }, }; } @@ -503,12 +807,9 @@ const manager = createStackE2eCleanupManager(); export function registerTempHome(home: { readonly dir: string; - readonly [Symbol.dispose]: () => void; + readonly dispose: () => Promise; }): void { - manager.registerHome({ - dir: home.dir, - dispose: () => home[Symbol.dispose](), - }); + manager.registerHome(home); } export function registerTempStackProject(project: { @@ -525,6 +826,6 @@ export function noteStackCliProjectHome(projectDir: string | undefined, homeDir: manager.associateHome(projectDir, homeDir); } -export async function cleanupRegisteredStackProjects(): Promise { - await manager.drain(); +export function cleanupRegisteredStackProjects(): Promise { + return manager.drain(); } diff --git a/apps/cli/tests/helpers/storage.ts b/apps/cli/tests/helpers/storage.ts index e5a81fc817..c24e933cec 100644 --- a/apps/cli/tests/helpers/storage.ts +++ b/apps/cli/tests/helpers/storage.ts @@ -1,8 +1,5 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; - import { BunServices } from "@effect/platform-bun"; -import { ConfigProvider, Effect, Layer, Option, Redacted, Stream } from "effect"; +import { ConfigProvider, Effect, FileSystem, Layer, Option, Path, Redacted, Stream } from "effect"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; import type { @@ -316,15 +313,28 @@ export function buildStorageStackApi( * Management API factory for api-keys. */ export function setupStorage(workdir: string, opts: SetupStorageOptions) { - if (opts.toml !== undefined) { - mkdirSync(join(workdir, "supabase"), { recursive: true }); - writeFileSync(join(workdir, "supabase", "config.toml"), opts.toml); - } - for (const [rel, content] of Object.entries(opts.files ?? {})) { - const abs = join(workdir, rel); - mkdirSync(dirname(abs), { recursive: true }); - writeFileSync(abs, content); - } + /* The fixture files are written once per setup, on first use: later acquisitions of the + returned layers never rewrite what a test changed since. */ + const writeWorkdirFiles = Effect.runSync( + Effect.cached( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + if (opts.toml !== undefined) { + yield* fs.makeDirectory(path.join(workdir, "supabase"), { recursive: true }); + yield* fs.writeFileString(path.join(workdir, "supabase", "config.toml"), opts.toml); + } + for (const [rel, content] of Object.entries(opts.files ?? {})) { + const abs = path.join(workdir, rel); + yield* fs.makeDirectory(path.dirname(abs), { recursive: true }); + yield* fs.writeFileString(abs, content); + } + }), + ), + ); + const workdirFiles = Layer.effectDiscard(writeWorkdirFiles).pipe( + Layer.provide(BunServices.layer), + ); const out = mockOutput({ format: opts.format ?? "text", @@ -482,7 +492,7 @@ export function setupStorage(workdir: string, opts: SetupStorageOptions) { ConfigProvider.fromEnvRecord(process.env, { preserveEmptyStrings: true }), ), ]), - ); + ).pipe(Layer.provide(workdirFiles)); return { layer, diff --git a/apps/cli/tests/helpers/target-default-warning.ts b/apps/cli/tests/helpers/target-default-warning.ts index 54f052ce78..322771d746 100644 --- a/apps/cli/tests/helpers/target-default-warning.ts +++ b/apps/cli/tests/helpers/target-default-warning.ts @@ -1,8 +1,6 @@ -import { mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - +import { BunServices } from "@effect/platform-bun"; import { expect } from "@effect/vitest"; -import { Cause, Effect, Exit } from "effect"; +import { Cause, Effect, Exit, FileSystem, Path } from "effect"; import { stripAnsi } from "./ansi.ts"; import { VALID_REF } from "./command-mocks.ts"; @@ -24,15 +22,18 @@ export const describeFailure = (exit: Exit.Exit): string | und }; /** Writes (`linked`) or removes the `supabase link` ref file in `workdir`. */ -export const setLinkedRefFile = (workdir: string, linked: boolean, ref = VALID_REF) => { - const tempDir = join(workdir, "supabase", ".temp"); - if (linked) { - mkdirSync(tempDir, { recursive: true }); - writeFileSync(join(tempDir, "project-ref"), ref); - } else { - rmSync(join(tempDir, "project-ref"), { force: true }); - } -}; +export const setLinkedRefFile = (workdir: string, linked: boolean, ref = VALID_REF) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const tempDir = path.join(workdir, "supabase", ".temp"); + if (linked) { + yield* fs.makeDirectory(tempDir, { recursive: true }); + yield* fs.writeFileString(path.join(tempDir, "project-ref"), ref); + } else { + yield* fs.remove(path.join(tempDir, "project-ref"), { force: true }); + } + }).pipe(Effect.provide(BunServices.layer)); /** * A bare run warns exactly once and otherwise prints what the `sameTarget` selector prints; every diff --git a/apps/cli/tests/lint/no-raw-management-api-url.unit.test.ts b/apps/cli/tests/lint/no-raw-management-api-url.unit.test.ts index 62d0f0a0f0..ef2e720be8 100644 --- a/apps/cli/tests/lint/no-raw-management-api-url.unit.test.ts +++ b/apps/cli/tests/lint/no-raw-management-api-url.unit.test.ts @@ -1,13 +1,10 @@ -import { spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { basename, join } from "node:path"; +import { BunServices } from "@effect/platform-bun"; +import { Effect, FileSystem, Path, Schema } from "effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; import { fileURLToPath } from "node:url"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url)); -const oxlintBin = join(repoRoot, "node_modules/.bin/oxlint"); -const pluginPath = join(repoRoot, "tools/oxlint/supabase-plugin.js"); const RULE_NAME = "no-raw-management-api-url"; const RULE = `supabase/${RULE_NAME}`; @@ -49,45 +46,74 @@ const allowed = { ].join("\n"), }; +const LintReport = Schema.Struct({ + diagnostics: Schema.Array(Schema.Struct({ filename: Schema.String, code: Schema.String })), + number_of_files: Schema.Finite, +}); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); +const decodeLintReport = Schema.decodeUnknownEffect(Schema.fromJsonString(LintReport)); + describe("no-raw-management-api-url", () => { let dir: string; let flaggedFiles: Set; let lintedFiles: Set; - beforeAll(() => { - dir = mkdtempSync(join(tmpdir(), "supabase-lint-rule-")); - mkdirSync(join(dir, "src")); - writeFileSync( - join(dir, ".oxlintrc.json"), - JSON.stringify({ - jsPlugins: [pluginPath], - categories: { correctness: "off" }, - rules: { [RULE]: "error" }, - }), - ); - for (const [name, source] of Object.entries({ ...flagged, ...allowed })) { - writeFileSync(join(dir, "src", name), `${source}\n`); - } + beforeAll(() => + Effect.runPromise( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const oxlintBin = path.join(repoRoot, "node_modules/.bin/oxlint"); + const pluginPath = path.join(repoRoot, "tools/oxlint/supabase-plugin.js"); + dir = yield* fs.makeTempDirectory({ prefix: "supabase-lint-rule-" }); + yield* fs.makeDirectory(path.join(dir, "src")); + yield* fs.writeFileString( + path.join(dir, ".oxlintrc.json"), + yield* encodeJson({ + jsPlugins: [pluginPath], + categories: { correctness: "off" }, + rules: { [RULE]: "error" }, + }), + ); + for (const [name, source] of Object.entries({ ...flagged, ...allowed })) { + yield* fs.writeFileString(path.join(dir, "src", name), `${source}\n`); + } - const result = spawnSync( - oxlintBin, - ["--config", join(dir, ".oxlintrc.json"), "--format", "json", join(dir, "src")], - { encoding: "utf8" }, - ); - const report = JSON.parse(result.stdout) as { - diagnostics: { filename: string; code: string }[]; - number_of_files: number; - }; - flaggedFiles = new Set( - report.diagnostics.filter((d) => d.code.includes(RULE_NAME)).map((d) => basename(d.filename)), - ); - lintedFiles = new Set(Object.keys({ ...flagged, ...allowed })); - expect(report.number_of_files).toBe(lintedFiles.size); - }); + const stdout = yield* spawner.string( + ChildProcess.make( + oxlintBin, + [ + "--config", + path.join(dir, ".oxlintrc.json"), + "--format", + "json", + path.join(dir, "src"), + ], + /* A plain child, not a process group: no process-group signals. */ + { detached: false, stdin: "ignore", stderr: "ignore" }, + ), + ); + const report = yield* decodeLintReport(stdout); + flaggedFiles = new Set( + report.diagnostics + .filter((d) => d.code.includes(RULE_NAME)) + .map((d) => path.basename(d.filename)), + ); + lintedFiles = new Set(Object.keys({ ...flagged, ...allowed })); + expect(report.number_of_files).toBe(lintedFiles.size); + }).pipe(Effect.provide(BunServices.layer)), + ), + ); - afterAll(() => { - rmSync(dir, { recursive: true, force: true }); - }); + afterAll(() => + Effect.runPromise( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(dir, { recursive: true, force: true }); + }).pipe(Effect.provide(BunServices.layer)), + ), + ); test.each(Object.keys(flagged))("flags %s", (name) => { expect(flaggedFiles.has(name)).toBe(true); diff --git a/apps/cli/tests/live-global-setup.ts b/apps/cli/tests/live-global-setup.ts index 71dbfc2839..48700a8451 100644 --- a/apps/cli/tests/live-global-setup.ts +++ b/apps/cli/tests/live-global-setup.ts @@ -1,9 +1,11 @@ import type { ProvidedContext } from "vitest"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { makeApiClient } from "@supabase/api/effect"; -import { Effect } from "effect"; +import { Data, Effect, FileSystem, Layer, Path, Predicate } from "effect"; import { FetchHttpClient } from "effect/http"; +import { cliConfigProviderLayer } from "../src/shared/config/cli-config-provider.layer.ts"; import "./helpers/live-provided-context.ts"; import { cleanupLiveEnvironment, provisionLiveEnvironment } from "./helpers/live-project.ts"; import { liveAccessToken, liveApiUrl, validateLiveConfig } from "./helpers/live-env.ts"; @@ -12,21 +14,71 @@ type LiveSetupContext = { provide: (key: K, value: ProvidedContext[K]) => void; }; +/** Vitest rejected a provided value; setup rejects with its original error. */ +class LiveProvideFailure extends Data.TaggedError("LiveProvideFailure")<{ + readonly cause: unknown; +}> {} + +/* + * What the live setup rejects with: a plain `Error` with the same message for a live configuration + * or project failure, also inside a lifecycle `AggregateError`. + */ +function thrownLiveFailure(error: unknown): unknown { + if ( + (Predicate.isTagged(error, "LiveProjectError") || + Predicate.isTagged(error, "LiveConfigError")) && + error instanceof Error + ) { + return new Error(error.message); + } + if (error instanceof AggregateError) { + const errors: Array = Array.from(error.errors, thrownLiveFailure); + return errors.every((inner, index) => inner === error.errors[index]) + ? error + : new AggregateError(errors, error.message); + } + return error; +} + /** Provision one disposable project for the entire serial live Vitest run. */ -export async function setup({ provide }: LiveSetupContext): Promise<() => Promise> { - validateLiveConfig(); - const { api, environment } = await Effect.runPromise( +export function setup({ provide }: LiveSetupContext): Promise<() => Promise> { + return Effect.runPromise( Effect.gen(function* () { - const api = yield* makeApiClient({ baseUrl: liveApiUrl(), accessToken: liveAccessToken() }); + yield* validateLiveConfig; + const api = yield* makeApiClient({ + baseUrl: yield* liveApiUrl, + accessToken: yield* liveAccessToken, + }); const environment = yield* provisionLiveEnvironment(api); - return { api, environment }; - }).pipe(Effect.provide(FetchHttpClient.layer)), + yield* Effect.try({ + try: () => { + provide("liveProject", environment.project); + provide("liveProfilePath", environment.profilePath); + }, + catch: (cause) => new LiveProvideFailure({ cause }), + }); + const services = yield* Effect.context(); + return () => + Effect.runPromiseWith(services)( + cleanupLiveEnvironment(api, environment).pipe( + Effect.catch((failure) => Effect.die(thrownLiveFailure(failure))), + ), + ); + }).pipe( + Effect.catchIf(Predicate.isTagged("LiveProvideFailure"), (failure) => + Effect.die(failure.cause), + ), + Effect.catch((failure) => Effect.die(thrownLiveFailure(failure))), + Effect.provide( + Layer.mergeAll( + FetchHttpClient.layer, + BunFileSystem.layer, + BunPath.layer, + cliConfigProviderLayer, + ), + ), + ), ); - provide("liveProject", environment.project); - provide("liveProfilePath", environment.profilePath); - return async () => { - await Effect.runPromise(cleanupLiveEnvironment(api, environment)); - }; } export default setup; diff --git a/apps/cli/tests/smoke-test-linux.ts b/apps/cli/tests/smoke-test-linux.ts index 0002a59ff0..cd3597d5ed 100644 --- a/apps/cli/tests/smoke-test-linux.ts +++ b/apps/cli/tests/smoke-test-linux.ts @@ -1,10 +1,15 @@ +import { BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -import path from "node:path"; +import { Console, Effect, Path } from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; import { isPublishableNpmTag, PUBLISHABLE_NPM_TAGS_HINT } from "../scripts/release-channels.ts"; import { describeError, runNpmTest } from "./helpers/npm-registry.ts"; -import { verifyExpectedShell } from "./helpers/release-shell.ts"; +import { + type ReleaseStepError, + releaseStep, + verifyExpectedShellEffect, +} from "./helpers/release-shell.ts"; const { values } = parseArgs({ options: { @@ -15,172 +20,196 @@ const { values } = parseArgs({ const version = values.version!; const tag = values.tag; -if (!isPublishableNpmTag(tag)) { - console.error(`Invalid --tag value: ${String(tag)}. Expected ${PUBLISHABLE_NPM_TAGS_HINT}.`); - process.exit(1); -} -const root = path.resolve(import.meta.dir, "../../.."); -const distDir = path.join(root, "dist"); - -const dispatchProbe = "supabase init --help 2>&1 | grep -q init"; - -interface TestResult { - name: string; - status: "pass" | "fail"; -} - -const results: TestResult[] = []; - -console.log(`\n${"=".repeat(60)}`); -console.log("Native binary tests"); -console.log("=".repeat(60)); - -{ - const arch = process.arch; - const name = `native-linux-${arch}`; - const binPath = path.join(root, "packages", `cli-linux-${arch}`, "bin", "supabase"); - - console.log(`[${name}] Running ${binPath} --version...`); - try { - const output = await $`${binPath} --version`.text(); - const trimmed = output.trim(); - const shellCheck = await verifyExpectedShell(binPath); - const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; - console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); - console.log(`[${name}] ${shellCheck.detail}`); - results.push({ name, status: passed ? "pass" : "fail" }); - } catch (e) { - console.log(`[${name}] FAIL —\n${describeError(e)}`); - results.push({ name, status: "fail" }); - } -} -console.log(`\n${"=".repeat(60)}`); -console.log("Docker-based Linux package tests"); -console.log("=".repeat(60)); +const main = Effect.gen(function* () { + if (!isPublishableNpmTag(tag)) { + yield* Console.error( + `Invalid --tag value: ${String(tag)}. Expected ${PUBLISHABLE_NPM_TAGS_HINT}.`, + ); + return yield* Effect.sync(() => process.exit(1)); + } + const path = yield* Path.Path; + const root = path.resolve(import.meta.dir, "../../.."); + const distDir = path.join(root, "dist"); -const hasDocker = await $`docker --version`.quiet().then( - () => true, - () => false, -); + const dispatchProbe = "supabase init --help 2>&1 | grep -q init"; -if (!hasDocker) { - console.log("[docker] SKIP — docker not found"); -} else { - interface DockerResult { + interface TestResult { name: string; - passed: boolean; - output: string; - } - - async function runDockerTest( - name: string, - image: string, - platform: string, - commands: string, - ): Promise { - console.log(`[${name}] Running...`); - for (let attempt = 1; attempt <= 2; attempt++) { - const result = - await $`docker run --rm --platform ${platform} -v ${distDir}:/dist:ro ${image} sh -c ${commands}` - .nothrow() - .quiet(); - const stdout = result.stdout.toString().trim(); - const stderr = result.stderr.toString().trim(); - if (result.exitCode === 0) { - const lastLine = stdout.split("\n").pop() ?? ""; - const passed = /^\d+\.\d+\.\d+/.test(lastLine); - console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${lastLine}`); - if (!passed && stderr) console.log(`[${name}] stderr: ${stderr}`); - return { name, passed, output: stdout }; - } - // Exit 125 is a docker daemon / container-start error, not a container - // exit code. Retry once before giving up. - if (result.exitCode === 125 && attempt === 1) { - console.log(`[${name}] docker exit 125, retrying once. stderr: ${stderr}`); - continue; - } - console.log(`[${name}] FAIL — exit ${result.exitCode}`); - if (stderr) console.log(`[${name}] stderr: ${stderr}`); - if (stdout) console.log(`[${name}] stdout: ${stdout}`); - return { name, passed: false, output: `${stdout}\n${stderr}`.trim() }; - } - return { name, passed: false, output: "unreachable" }; + status: "pass" | "fail"; } - const jobs: Promise[] = []; - - for (const arch of ["arm64", "amd64"] as const) { - const dockerPlatform = `linux/${arch}`; - - jobs.push( - runDockerTest( - `linux-${arch}-tarball`, - "debian:bookworm-slim", - dockerPlatform, - `tar -xzf /dist/supabase_${version}_linux_${arch}.tar.gz -C /usr/local/bin && supabase --version && ${dispatchProbe}`, + const results: TestResult[] = []; + + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Native binary tests"); + yield* Console.log("=".repeat(60)); + + { + const arch = process.arch; + const name = `native-linux-${arch}`; + const binPath = path.join(root, "packages", `cli-linux-${arch}`, "bin", "supabase"); + + yield* Console.log(`[${name}] Running ${binPath} --version...`); + yield* Effect.gen(function* () { + const output = yield* releaseStep(() => $`${binPath} --version`.text()); + const trimmed = output.trim(); + const shellCheck = yield* verifyExpectedShellEffect(binPath); + const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; + yield* Console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); + yield* Console.log(`[${name}] ${shellCheck.detail}`); + results.push({ name, status: passed ? "pass" : "fail" }); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.log(`[${name}] FAIL —\n${describeError(e.cause)}`); + results.push({ name, status: "fail" }); + }), ), ); + } - jobs.push( - runDockerTest( - `linux-${arch}-deb`, - "debian:bookworm-slim", - dockerPlatform, - `dpkg -i /dist/supabase_${version}_linux_${arch}.deb && supabase --version && ${dispatchProbe}`, - ), - ); + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Docker-based Linux package tests"); + yield* Console.log("=".repeat(60)); + + const hasDocker = yield* releaseStep(() => $`docker --version`.quiet()).pipe( + Effect.match({ + onSuccess: () => true, + onFailure: () => false, + }), + ); + + if (!hasDocker) { + yield* Console.log("[docker] SKIP — docker not found"); + } else { + interface DockerResult { + name: string; + passed: boolean; + output: string; + } - jobs.push( - runDockerTest( - `linux-${arch}-rpm`, - "amazonlinux:2023", - dockerPlatform, - `rpm -ivh /dist/supabase_${version}_linux_${arch}.rpm && supabase --version && ${dispatchProbe}`, - ), - ); + const runDockerTest = Effect.fnUntraced(function* ( + name: string, + image: string, + platform: string, + commands: string, + ) { + yield* Console.log(`[${name}] Running...`); + for (let attempt = 1; attempt <= 2; attempt++) { + const result = yield* releaseStep(() => + $`docker run --rm --platform ${platform} -v ${distDir}:/dist:ro ${image} sh -c ${commands}` + .nothrow() + .quiet(), + ); + const stdout = result.stdout.toString().trim(); + const stderr = result.stderr.toString().trim(); + if (result.exitCode === 0) { + const lastLine = stdout.split("\n").pop() ?? ""; + const passed = /^\d+\.\d+\.\d+/.test(lastLine); + yield* Console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${lastLine}`); + if (!passed && stderr) yield* Console.log(`[${name}] stderr: ${stderr}`); + return { name, passed, output: stdout }; + } + // Exit 125 is a docker daemon / container-start error, not a container + // exit code. Retry once before giving up. + if (result.exitCode === 125 && attempt === 1) { + yield* Console.log(`[${name}] docker exit 125, retrying once. stderr: ${stderr}`); + continue; + } + yield* Console.log(`[${name}] FAIL — exit ${result.exitCode}`); + if (stderr) yield* Console.log(`[${name}] stderr: ${stderr}`); + if (stdout) yield* Console.log(`[${name}] stdout: ${stdout}`); + return { name, passed: false, output: `${stdout}\n${stderr}`.trim() }; + } + return { name, passed: false, output: "unreachable" }; + }); + + const jobs: Effect.Effect[] = []; + + for (const arch of ["arm64", "amd64"] as const) { + const dockerPlatform = `linux/${arch}`; + + jobs.push( + runDockerTest( + `linux-${arch}-tarball`, + "debian:bookworm-slim", + dockerPlatform, + `tar -xzf /dist/supabase_${version}_linux_${arch}.tar.gz -C /usr/local/bin && supabase --version && ${dispatchProbe}`, + ), + ); + + jobs.push( + runDockerTest( + `linux-${arch}-deb`, + "debian:bookworm-slim", + dockerPlatform, + `dpkg -i /dist/supabase_${version}_linux_${arch}.deb && supabase --version && ${dispatchProbe}`, + ), + ); + + jobs.push( + runDockerTest( + `linux-${arch}-rpm`, + "amazonlinux:2023", + dockerPlatform, + `rpm -ivh /dist/supabase_${version}_linux_${arch}.rpm && supabase --version && ${dispatchProbe}`, + ), + ); + + jobs.push( + runDockerTest( + `linux-${arch}-apk`, + "alpine:3.21", + dockerPlatform, + `apk add --allow-untrusted /dist/supabase_${version}_linux_${arch}.apk && supabase --version && ${dispatchProbe}`, + ), + ); + } - jobs.push( - runDockerTest( - `linux-${arch}-apk`, - "alpine:3.21", - dockerPlatform, - `apk add --allow-untrusted /dist/supabase_${version}_linux_${arch}.apk && supabase --version && ${dispatchProbe}`, - ), - ); + const dockerResults = yield* Effect.all(jobs, { concurrency: "unbounded" }); + for (const r of dockerResults) { + results.push({ name: r.name, status: r.passed ? "pass" : "fail" }); + } } - const dockerResults = await Promise.all(jobs); - for (const r of dockerResults) { - results.push({ name: r.name, status: r.passed ? "pass" : "fail" }); + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("npm (Verdaccio) test"); + yield* Console.log("=".repeat(60)); + + yield* Effect.gen(function* () { + const npmPassed = yield* releaseStep(() => runNpmTest(version, tag)); + results.push({ name: "npm", status: npmPassed ? "pass" : "fail" }); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.error(`[npm] Error:\n${describeError(e.cause)}`); + results.push({ name: "npm", status: "fail" }); + }), + ), + ); + + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Linux Smoke Test Summary"); + yield* Console.log("=".repeat(60)); + + for (const r of results) { + yield* Console.log(` ${r.status === "pass" ? "PASS" : "FAIL"} ${r.name}`); } -} - -console.log(`\n${"=".repeat(60)}`); -console.log("npm (Verdaccio) test"); -console.log("=".repeat(60)); - -try { - const npmPassed = await runNpmTest(version, tag); - results.push({ name: "npm", status: npmPassed ? "pass" : "fail" }); -} catch (e) { - console.error(`[npm] Error:\n${describeError(e)}`); - results.push({ name: "npm", status: "fail" }); -} - -console.log(`\n${"=".repeat(60)}`); -console.log("Linux Smoke Test Summary"); -console.log("=".repeat(60)); -for (const r of results) { - console.log(` ${r.status === "pass" ? "PASS" : "FAIL"} ${r.name}`); -} + const passed = results.filter((r) => r.status === "pass").length; + const failed = results.filter((r) => r.status === "fail").length; -const passed = results.filter((r) => r.status === "pass").length; -const failed = results.filter((r) => r.status === "fail").length; + yield* Console.log(`\n${passed} passed, ${failed} failed out of ${results.length} tests`); -console.log(`\n${passed} passed, ${failed} failed out of ${results.length} tests`); + if (failed > 0) { + return yield* Effect.sync(() => process.exit(1)); + } +}); -if (failed > 0) { - process.exit(1); -} +await Effect.runPromise( + main.pipe( + Effect.catch((e) => Effect.die(e.cause)), + Effect.provide(BunPath.layer), + ), +); diff --git a/apps/cli/tests/smoke-test-macos.ts b/apps/cli/tests/smoke-test-macos.ts index fed05328b0..67c52cc47c 100644 --- a/apps/cli/tests/smoke-test-macos.ts +++ b/apps/cli/tests/smoke-test-macos.ts @@ -1,13 +1,12 @@ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -import { existsSync } from "node:fs"; -import { mkdir } from "node:fs/promises"; -import path from "node:path"; +import { Cause, Console, Data, Effect, Exit, FileSystem, Layer, Path, Predicate } from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; import { isPublishableNpmTag, PUBLISHABLE_NPM_TAGS_HINT } from "../scripts/release-channels.ts"; import { verifyMacSignature } from "./helpers/macos-signature.ts"; import { createTmpDir, describeError, runNpmTest } from "./helpers/npm-registry.ts"; -import { verifyExpectedShell } from "./helpers/release-shell.ts"; +import { releaseStep, verifyExpectedShellEffect } from "./helpers/release-shell.ts"; const { values } = parseArgs({ options: { @@ -18,139 +17,237 @@ const { values } = parseArgs({ const version = values.version!; const tag = values.tag; -if (!isPublishableNpmTag(tag)) { - console.error(`Invalid --tag value: ${String(tag)}. Expected ${PUBLISHABLE_NPM_TAGS_HINT}.`); - process.exit(1); -} -const root = path.resolve(import.meta.dir, "../../.."); - -interface TestResult { - name: string; - status: "pass" | "fail"; -} -const results: TestResult[] = []; - -console.log(`\n${"=".repeat(60)}`); -console.log("Native binary tests"); -console.log("=".repeat(60)); - -{ - const arch = process.arch; - const name = `native-darwin-${arch}`; - const binPath = path.join(root, "packages", `cli-darwin-${arch}`, "bin", "supabase"); - - console.log(`[${name}] Running ${binPath} --version...`); - try { - const output = await $`${binPath} --version`.text(); - const trimmed = output.trim(); - const shellCheck = await verifyExpectedShell(binPath); - const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; - console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); - console.log(`[${name}] ${shellCheck.detail}`); - results.push({ name, status: passed ? "pass" : "fail" }); - } catch (e) { - console.log(`[${name}] FAIL —\n${describeError(e)}`); - results.push({ name, status: "fail" }); +/** Removing the tap failed after the brew steps had already failed; both causes are kept. */ +class TapDisposalSuppressed extends Data.TaggedError("TapDisposalSuppressed")<{ + readonly error: Cause.Cause; + readonly suppressed: Cause.Cause; +}> {} + +/* What a brew step reports: the wrapped error, or a SuppressedError as `await using` builds. */ +function thrownValue(failure: unknown): unknown { + if (failure instanceof TapDisposalSuppressed) { + return new SuppressedError( + thrownValue(Cause.squash(failure.error)), + thrownValue(Cause.squash(failure.suppressed)), + ); + } + if ( + (Predicate.isTagged(failure, "ReleaseStepError") || + Predicate.isTagged(failure, "PlatformError")) && + Predicate.hasProperty(failure, "cause") + ) { + return failure.cause; } + return failure; } -{ - const arch = process.arch; - const binDir = path.join(root, "packages", `cli-darwin-${arch}`, "bin"); - const binaries = ["supabase"]; - if (existsSync(path.join(binDir, "supabase-go"))) { - binaries.push("supabase-go"); +/** `try { body } finally { cleanup }`: a cleanup failure replaces the body's outcome. */ +const withFinally = ( + body: Effect.Effect, + cleanup: Effect.Effect, +): Effect.Effect => + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const exit = yield* Effect.exit(restore(body)); + yield* cleanup; + return yield* exit; + }), + ); + +const main = Effect.gen(function* () { + if (!isPublishableNpmTag(tag)) { + yield* Console.error( + `Invalid --tag value: ${String(tag)}. Expected ${PUBLISHABLE_NPM_TAGS_HINT}.`, + ); + return yield* Effect.sync(() => process.exit(1)); } + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = path.resolve(import.meta.dir, "../../.."); - for (const binary of binaries) { - const name = `native-darwin-${arch}-signature-${binary}`; - const binPath = path.join(binDir, binary); - console.log(`[${name}] Verifying signature of ${binPath}...`); - try { - const sig = await verifyMacSignature(binPath); - console.log(`[${name}] ${sig.passed ? "PASS" : "FAIL"} — ${sig.detail}`); - results.push({ name, status: sig.passed ? "pass" : "fail" }); - } catch (e) { - console.log(`[${name}] FAIL —\n${describeError(e)}`); - results.push({ name, status: "fail" }); - } + interface TestResult { + name: string; + status: "pass" | "fail"; } -} - -console.log(`\n${"=".repeat(60)}`); -console.log("npm (Verdaccio) test"); -console.log("=".repeat(60)); - -try { - const npmPassed = await runNpmTest(version, tag); - results.push({ name: "npm", status: npmPassed ? "pass" : "fail" }); -} catch (e) { - console.error(`[npm] Error:\n${describeError(e)}`); - results.push({ name: "npm", status: "fail" }); -} - -console.log(`\n${"=".repeat(60)}`); -console.log("Homebrew test"); -console.log("=".repeat(60)); - -const hasBrew = await $`brew --version`.quiet().then( - () => true, - () => false, -); - -if (!hasBrew) { - console.log("[brew] SKIP — brew not found"); -} else { - try { - console.log("Generating Homebrew formula..."); - await $`bun run apps/cli/scripts/update-homebrew.ts --version ${version} --local`.cwd(root); - // Create a local git-backed tap - await using tap = await createTmpDir("brew-smoke-"); - await mkdir(path.join(tap.path, "Formula")); - await $`cp ${path.join(root, "dist", "supabase.rb")} ${path.join(tap.path, "Formula", "supabase.rb")}`; - await $`git -C ${tap.path} init`.quiet(); - await $`git -C ${tap.path} add .`.quiet(); - await $`git -C ${tap.path} commit -m init`.quiet(); + const results: TestResult[] = []; - console.log("Installing via Homebrew..."); - await $`brew tap --force supabase/test-tap ${tap.path}`; + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Native binary tests"); + yield* Console.log("=".repeat(60)); - try { - await $`brew install supabase/test-tap/supabase`; + { + const arch = process.arch; + const name = `native-darwin-${arch}`; + const binPath = path.join(root, "packages", `cli-darwin-${arch}`, "bin", "supabase"); - const output = await $`supabase --version`.text(); + yield* Console.log(`[${name}] Running ${binPath} --version...`); + yield* Effect.gen(function* () { + const output = yield* releaseStep(() => $`${binPath} --version`.text()); const trimmed = output.trim(); - const shellCheck = await verifyExpectedShell("supabase"); + const shellCheck = yield* verifyExpectedShellEffect(binPath); const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; + yield* Console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); + yield* Console.log(`[${name}] ${shellCheck.detail}`); + results.push({ name, status: passed ? "pass" : "fail" }); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.log(`[${name}] FAIL —\n${describeError(e.cause)}`); + results.push({ name, status: "fail" }); + }), + ), + ); + } + + { + const arch = process.arch; + const binDir = path.join(root, "packages", `cli-darwin-${arch}`, "bin"); + const binaries = ["supabase"]; + if ( + yield* fs.exists(path.join(binDir, "supabase-go")).pipe(Effect.orElseSucceed(() => false)) + ) { + binaries.push("supabase-go"); + } - console.log(`[brew] ${passed ? "PASS" : "FAIL"} — supabase --version: ${trimmed}`); - console.log(`[brew] ${shellCheck.detail}`); - results.push({ name: "brew", status: passed ? "pass" : "fail" }); - } finally { - await $`brew uninstall supabase`.nothrow(); - await $`brew untap supabase/test-tap`.nothrow(); + for (const binary of binaries) { + const name = `native-darwin-${arch}-signature-${binary}`; + const binPath = path.join(binDir, binary); + yield* Console.log(`[${name}] Verifying signature of ${binPath}...`); + yield* Effect.gen(function* () { + const sig = yield* verifyMacSignature(binPath); + yield* Console.log(`[${name}] ${sig.passed ? "PASS" : "FAIL"} — ${sig.detail}`); + results.push({ name, status: sig.passed ? "pass" : "fail" }); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.log(`[${name}] FAIL —\n${describeError(e.cause)}`); + results.push({ name, status: "fail" }); + }), + ), + ); } - } catch (e) { - console.error(`[brew] Error:\n${describeError(e)}`); - results.push({ name: "brew", status: "fail" }); } -} -console.log(`\n${"=".repeat(60)}`); -console.log("macOS Smoke Test Summary"); -console.log("=".repeat(60)); + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("npm (Verdaccio) test"); + yield* Console.log("=".repeat(60)); + + yield* Effect.gen(function* () { + const npmPassed = yield* releaseStep(() => runNpmTest(version, tag)); + results.push({ name: "npm", status: npmPassed ? "pass" : "fail" }); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.error(`[npm] Error:\n${describeError(e.cause)}`); + results.push({ name: "npm", status: "fail" }); + }), + ), + ); + + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Homebrew test"); + yield* Console.log("=".repeat(60)); + + const hasBrew = yield* releaseStep(() => $`brew --version`.quiet()).pipe( + Effect.match({ + onSuccess: () => true, + onFailure: () => false, + }), + ); + + if (!hasBrew) { + yield* Console.log("[brew] SKIP — brew not found"); + } else { + yield* Effect.gen(function* () { + yield* Console.log("Generating Homebrew formula..."); + yield* releaseStep(() => + $`bun run apps/cli/scripts/update-homebrew.ts --version ${version} --local`.cwd(root), + ); + + // Create a local git-backed tap + const useTap = (tap: AsyncDisposable & { path: string }) => + Effect.gen(function* () { + yield* fs.makeDirectory(path.join(tap.path, "Formula")); + yield* releaseStep( + () => + $`cp ${path.join(root, "dist", "supabase.rb")} ${path.join(tap.path, "Formula", "supabase.rb")}`, + ); + yield* releaseStep(() => $`git -C ${tap.path} init`.quiet()); + yield* releaseStep(() => $`git -C ${tap.path} add .`.quiet()); + yield* releaseStep(() => $`git -C ${tap.path} commit -m init`.quiet()); + + yield* Console.log("Installing via Homebrew..."); + yield* releaseStep(() => $`brew tap --force supabase/test-tap ${tap.path}`); + + yield* withFinally( + Effect.gen(function* () { + yield* releaseStep(() => $`brew install supabase/test-tap/supabase`); + + const output = yield* releaseStep(() => $`supabase --version`.text()); + const trimmed = output.trim(); + const shellCheck = yield* verifyExpectedShellEffect("supabase"); + const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; + + yield* Console.log( + `[brew] ${passed ? "PASS" : "FAIL"} — supabase --version: ${trimmed}`, + ); + yield* Console.log(`[brew] ${shellCheck.detail}`); + results.push({ name: "brew", status: passed ? "pass" : "fail" }); + }), + Effect.gen(function* () { + yield* releaseStep(() => $`brew uninstall supabase`.nothrow()); + yield* releaseStep(() => $`brew untap supabase/test-tap`.nothrow()); + }), + ); + }); + /* `await using tap`: a failed removal replaces the outcome, or suppresses a failed one. */ + yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const tap = yield* releaseStep(() => createTmpDir("brew-smoke-")); + const used = yield* Effect.exit(restore(useTap(tap))); + const disposed = yield* Effect.exit(releaseStep(() => tap[Symbol.asyncDispose]())); + if (Exit.isFailure(disposed) && Exit.isFailure(used)) { + /* An interrupted use keeps its interruption, with the removal failure alongside. */ + return yield* Cause.hasInterrupts(used.cause) + ? Effect.failCause(Cause.combine(used.cause, disposed.cause)) + : Effect.fail( + new TapDisposalSuppressed({ error: disposed.cause, suppressed: used.cause }), + ); + } + yield* disposed; + return yield* used; + }), + ); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.error(`[brew] Error:\n${describeError(thrownValue(e))}`); + results.push({ name: "brew", status: "fail" }); + }), + ), + ); + } -for (const r of results) { - console.log(` ${r.status === "pass" ? "PASS" : "FAIL"} ${r.name}`); -} + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("macOS Smoke Test Summary"); + yield* Console.log("=".repeat(60)); -const passed = results.filter((r) => r.status === "pass").length; -const failed = results.filter((r) => r.status === "fail").length; + for (const r of results) { + yield* Console.log(` ${r.status === "pass" ? "PASS" : "FAIL"} ${r.name}`); + } -console.log(`\n${passed} passed, ${failed} failed out of ${results.length} tests`); + const passed = results.filter((r) => r.status === "pass").length; + const failed = results.filter((r) => r.status === "fail").length; -if (failed > 0) { - process.exit(1); -} + yield* Console.log(`\n${passed} passed, ${failed} failed out of ${results.length} tests`); + + if (failed > 0) { + return yield* Effect.sync(() => process.exit(1)); + } +}); + +await Effect.runPromise( + main.pipe(Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer))), +); diff --git a/apps/cli/tests/smoke-test-windows.ts b/apps/cli/tests/smoke-test-windows.ts index 19c65060ac..b0b02dba37 100644 --- a/apps/cli/tests/smoke-test-windows.ts +++ b/apps/cli/tests/smoke-test-windows.ts @@ -1,11 +1,10 @@ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { Console, Effect, FileSystem, Layer, Path } from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; import { isPublishableNpmTag, PUBLISHABLE_NPM_TAGS_HINT } from "../scripts/release-channels.ts"; -import { verifyExpectedShell } from "./helpers/release-shell.ts"; +import { releaseStep, verifyExpectedShellEffect } from "./helpers/release-shell.ts"; const { values } = parseArgs({ options: { @@ -16,130 +15,185 @@ const { values } = parseArgs({ const version = values.version!; const tag = values.tag; -if (!isPublishableNpmTag(tag)) { - console.error(`Invalid --tag value: ${String(tag)}. Expected ${PUBLISHABLE_NPM_TAGS_HINT}.`); - process.exit(1); -} -const root = path.resolve(import.meta.dir, "../../.."); - -async function gitBashPath(filePath: string) { - return process.platform === "win32" ? (await $`cygpath -u ${filePath}`.text()).trim() : filePath; -} - -interface TestResult { - name: string; - status: "pass" | "fail"; -} - -const results: TestResult[] = []; - -console.log(`\n${"=".repeat(60)}`); -console.log("Native binary tests"); -console.log("=".repeat(60)); - -const arch = process.arch === "arm64" ? "arm64" : "x64"; - -{ - const name = `native-windows-${arch}`; - const binPath = path.join(root, "packages", `cli-windows-${arch}`, "bin", "supabase.exe"); - - console.log(`[${name}] Running ${binPath} --version...`); - try { - const output = await $`${binPath} --version`.text(); - const trimmed = output.trim(); - const shellCheck = await verifyExpectedShell(binPath); - const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; - console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); - console.log(`[${name}] ${shellCheck.detail}`); - results.push({ name, status: passed ? "pass" : "fail" }); - } catch (e) { - console.log(`[${name}] FAIL — ${e}`); - results.push({ name, status: "fail" }); - } -} - -console.log(`\n${"=".repeat(60)}`); -console.log("Release tarball test"); -console.log("=".repeat(60)); - -{ - const archiveArch = arch === "arm64" ? "arm64" : "amd64"; - const name = `windows-${archiveArch}-tarball`; - const archivePath = path.join(root, "dist", `supabase_${version}_windows_${archiveArch}.tar.gz`); - const extractDir = await mkdtemp(path.join(tmpdir(), "supabase-windows-tarball-")); - - console.log(`[${name}] Extracting ${archivePath}...`); - try { - await $`tar -xzf ${await gitBashPath(archivePath)} -C ${await gitBashPath(extractDir)}`; - const binPath = path.join(extractDir, "supabase.exe"); - const output = await $`${binPath} --version`.text(); - const trimmed = output.trim(); - const shellCheck = await verifyExpectedShell(binPath); - const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; - - console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); - console.log(`[${name}] ${shellCheck.detail}`); - results.push({ name, status: passed ? "pass" : "fail" }); - } catch (e) { - console.error(`[${name}] Error: ${e}`); - results.push({ name, status: "fail" }); - } finally { - await rm(extractDir, { recursive: true, force: true }); + +const gitBashPath = Effect.fnUntraced(function* (filePath: string) { + return process.platform === "win32" + ? (yield* releaseStep(() => $`cygpath -u ${filePath}`.text())).trim() + : filePath; +}); + +/** `try { body } finally { cleanup }`: a cleanup failure replaces the body's outcome. */ +const withFinally = ( + body: Effect.Effect, + cleanup: Effect.Effect, +): Effect.Effect => + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const exit = yield* Effect.exit(restore(body)); + yield* cleanup; + return yield* exit; + }), + ); + +const main = Effect.gen(function* () { + if (!isPublishableNpmTag(tag)) { + yield* Console.error( + `Invalid --tag value: ${String(tag)}. Expected ${PUBLISHABLE_NPM_TAGS_HINT}.`, + ); + return yield* Effect.sync(() => process.exit(1)); } -} + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = path.resolve(import.meta.dir, "../../.."); -console.log(`\n${"=".repeat(60)}`); -console.log("Scoop test"); -console.log("=".repeat(60)); + interface TestResult { + name: string; + status: "pass" | "fail"; + } -const hasScoop = await $`scoop --version`.quiet().then( - () => true, - () => false, -); + const results: TestResult[] = []; -if (!hasScoop) { - console.log("[scoop] SKIP — scoop not found"); -} else { - const manifest = path.join(root, "dist", "supabase.json"); + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Native binary tests"); + yield* Console.log("=".repeat(60)); - try { - console.log("Generating Scoop manifest..."); - await $`bun run apps/cli/scripts/update-scoop.ts --version ${version} --local`.cwd(root); + const arch = process.arch === "arm64" ? "arm64" : "x64"; - console.log("Installing via Scoop..."); - await $`scoop install ${manifest}`; + { + const name = `native-windows-${arch}`; + const binPath = path.join(root, "packages", `cli-windows-${arch}`, "bin", "supabase.exe"); - try { - const output = await $`supabase --version`.text(); + yield* Console.log(`[${name}] Running ${binPath} --version...`); + yield* Effect.gen(function* () { + const output = yield* releaseStep(() => $`${binPath} --version`.text()); const trimmed = output.trim(); - const shellCheck = await verifyExpectedShell("supabase"); + const shellCheck = yield* verifyExpectedShellEffect(binPath); const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; + yield* Console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); + yield* Console.log(`[${name}] ${shellCheck.detail}`); + results.push({ name, status: passed ? "pass" : "fail" }); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.log(`[${name}] FAIL — ${e.cause}`); + results.push({ name, status: "fail" }); + }), + ), + ); + } - console.log(`[scoop] ${passed ? "PASS" : "FAIL"} — supabase --version: ${trimmed}`); - console.log(`[scoop] ${shellCheck.detail}`); - results.push({ name: "scoop", status: passed ? "pass" : "fail" }); - } finally { - await $`scoop uninstall supabase`.nothrow(); - } - } catch (e) { - console.error(`[scoop] Error: ${e}`); - results.push({ name: "scoop", status: "fail" }); + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Release tarball test"); + yield* Console.log("=".repeat(60)); + + { + const archiveArch = arch === "arm64" ? "arm64" : "amd64"; + const name = `windows-${archiveArch}-tarball`; + const archivePath = path.join( + root, + "dist", + `supabase_${version}_windows_${archiveArch}.tar.gz`, + ); + const extractDir = yield* fs.makeTempDirectory({ prefix: "supabase-windows-tarball-" }); + + yield* Console.log(`[${name}] Extracting ${archivePath}...`); + yield* Effect.onExit( + Effect.gen(function* () { + const archiveBashPath = yield* gitBashPath(archivePath); + const extractBashPath = yield* gitBashPath(extractDir); + yield* releaseStep(() => $`tar -xzf ${archiveBashPath} -C ${extractBashPath}`); + const binPath = path.join(extractDir, "supabase.exe"); + const output = yield* releaseStep(() => $`${binPath} --version`.text()); + const trimmed = output.trim(); + const shellCheck = yield* verifyExpectedShellEffect(binPath); + const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; + + yield* Console.log(`[${name}] ${passed ? "PASS" : "FAIL"} — ${trimmed}`); + yield* Console.log(`[${name}] ${shellCheck.detail}`); + results.push({ name, status: passed ? "pass" : "fail" }); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.error(`[${name}] Error: ${e.cause}`); + results.push({ name, status: "fail" }); + }), + ), + ), + () => fs.remove(extractDir, { recursive: true, force: true }), + ); } -} -console.log(`\n${"=".repeat(60)}`); -console.log("Windows Smoke Test Summary"); -console.log("=".repeat(60)); + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Scoop test"); + yield* Console.log("=".repeat(60)); + + const hasScoop = yield* releaseStep(() => $`scoop --version`.quiet()).pipe( + Effect.match({ + onSuccess: () => true, + onFailure: () => false, + }), + ); + + if (!hasScoop) { + yield* Console.log("[scoop] SKIP — scoop not found"); + } else { + const manifest = path.join(root, "dist", "supabase.json"); + + yield* Effect.gen(function* () { + yield* Console.log("Generating Scoop manifest..."); + yield* releaseStep(() => + $`bun run apps/cli/scripts/update-scoop.ts --version ${version} --local`.cwd(root), + ); + + yield* Console.log("Installing via Scoop..."); + yield* releaseStep(() => $`scoop install ${manifest}`); + + yield* withFinally( + Effect.gen(function* () { + const output = yield* releaseStep(() => $`supabase --version`.text()); + const trimmed = output.trim(); + const shellCheck = yield* verifyExpectedShellEffect("supabase"); + const passed = /^\d+\.\d+\.\d+/.test(trimmed) && shellCheck.passed; + + yield* Console.log( + `[scoop] ${passed ? "PASS" : "FAIL"} — supabase --version: ${trimmed}`, + ); + yield* Console.log(`[scoop] ${shellCheck.detail}`); + results.push({ name: "scoop", status: passed ? "pass" : "fail" }); + }), + releaseStep(() => $`scoop uninstall supabase`.nothrow()), + ); + }).pipe( + Effect.catch((e) => + Effect.gen(function* () { + yield* Console.error(`[scoop] Error: ${e.cause}`); + results.push({ name: "scoop", status: "fail" }); + }), + ), + ); + } + + yield* Console.log(`\n${"=".repeat(60)}`); + yield* Console.log("Windows Smoke Test Summary"); + yield* Console.log("=".repeat(60)); -for (const r of results) { - console.log(` ${r.status === "pass" ? "PASS" : "FAIL"} ${r.name}`); -} + for (const r of results) { + yield* Console.log(` ${r.status === "pass" ? "PASS" : "FAIL"} ${r.name}`); + } -const passed = results.filter((r) => r.status === "pass").length; -const failed = results.filter((r) => r.status === "fail").length; + const passed = results.filter((r) => r.status === "pass").length; + const failed = results.filter((r) => r.status === "fail").length; -console.log(`\n${passed} passed, ${failed} failed out of ${results.length} tests`); + yield* Console.log(`\n${passed} passed, ${failed} failed out of ${results.length} tests`); -if (failed > 0) { - process.exit(1); -} + if (failed > 0) { + return yield* Effect.sync(() => process.exit(1)); + } +}); + +await Effect.runPromise( + main.pipe( + Effect.catch((e) => Effect.die(e.cause)), + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + ), +); diff --git a/apps/cli/tests/smoke-test.ts b/apps/cli/tests/smoke-test.ts index 73d7b3be20..bcd620add1 100644 --- a/apps/cli/tests/smoke-test.ts +++ b/apps/cli/tests/smoke-test.ts @@ -1,7 +1,9 @@ -import path from "node:path"; +import { BunPath } from "@effect/platform-bun"; +import { Console, Effect, Path } from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; import { isPublishableNpmTag, PUBLISHABLE_NPM_TAGS_HINT } from "../scripts/release-channels.ts"; +import { ReleaseStepError } from "./helpers/release-shell.ts"; const { values } = parseArgs({ options: { @@ -12,32 +14,49 @@ const { values } = parseArgs({ const version = values.version!; const tag = values.tag; -if (!isPublishableNpmTag(tag)) { - console.error(`Invalid --tag value: ${String(tag)}. Expected ${PUBLISHABLE_NPM_TAGS_HINT}.`); - process.exit(1); -} -const testsDir = import.meta.dir; - -const platformScripts: Record = { - linux: "smoke-test-linux.ts", - darwin: "smoke-test-macos.ts", - win32: "smoke-test-windows.ts", -}; - -const script = platformScripts[process.platform]; -if (!script) { - console.error(`Unsupported platform: ${process.platform}`); - process.exit(1); -} - -const scriptPath = path.join(testsDir, script); -console.log(`Detected platform: ${process.platform} — running ${script}\n`); - -const proc = Bun.spawn(["bun", "run", scriptPath, "--version", version, "--tag", tag], { - stdout: "inherit", - stderr: "inherit", - env: process.env, + +const main = Effect.gen(function* () { + if (!isPublishableNpmTag(tag)) { + yield* Console.error( + `Invalid --tag value: ${String(tag)}. Expected ${PUBLISHABLE_NPM_TAGS_HINT}.`, + ); + return yield* Effect.sync(() => process.exit(1)); + } + const testsDir = import.meta.dir; + + const platformScripts: Record = { + linux: "smoke-test-linux.ts", + darwin: "smoke-test-macos.ts", + win32: "smoke-test-windows.ts", + }; + + const script = platformScripts[process.platform]; + if (!script) { + yield* Console.error(`Unsupported platform: ${process.platform}`); + return yield* Effect.sync(() => process.exit(1)); + } + + const path = yield* Path.Path; + const scriptPath = path.join(testsDir, script); + yield* Console.log(`Detected platform: ${process.platform} — running ${script}\n`); + + const proc = yield* Effect.try({ + try: () => + Bun.spawn(["bun", "run", scriptPath, "--version", version, "--tag", tag], { + stdout: "inherit", + stderr: "inherit", + env: process.env, + }), + catch: (cause) => new ReleaseStepError({ cause }), + }); + + const exitCode = yield* Effect.promise(() => proc.exited); + return yield* Effect.sync(() => process.exit(exitCode)); }); -const exitCode = await proc.exited; -process.exit(exitCode); +await Effect.runPromise( + main.pipe( + Effect.catch((error) => Effect.die(error.cause)), + Effect.provide(BunPath.layer), + ), +); diff --git a/apps/cli/vitest.config.ts b/apps/cli/vitest.config.ts index 531aefc51c..be34ee20ea 100644 --- a/apps/cli/vitest.config.ts +++ b/apps/cli/vitest.config.ts @@ -1,14 +1,16 @@ -import { readFileSync } from "node:fs"; +import { BunFileSystem } from "@effect/platform-bun"; +import { Config, Effect, FileSystem, Schema } from "effect"; import { defaultClientConditions, defaultServerConditions } from "vite"; import { configDefaults, defineConfig } from "vitest/config"; import { E2eSequencer } from "./tests/e2e-sequencer.ts"; /** E2e files that run concurrently; each stack project picks free ports and its own project id. */ -const e2eWorkers = Number(process.env.SUPABASE_E2E_WORKERS || "1"); +const e2eWorkersSetting = Effect.runSync( + Config.String("SUPABASE_E2E_WORKERS").pipe(Config.withDefault("1")), +); +const e2eWorkers = Number(e2eWorkersSetting); if (!Number.isSafeInteger(e2eWorkers) || e2eWorkers < 1) { - throw new Error( - `SUPABASE_E2E_WORKERS must be a positive integer, got "${process.env.SUPABASE_E2E_WORKERS}"`, - ); + throw new Error(`SUPABASE_E2E_WORKERS must be a positive integer, got "${e2eWorkersSetting}"`); } /** @@ -27,6 +29,15 @@ const E2E_EXCLUSIVE_FILES = [ "src/commands/db/start/start.roles.e2e.test.ts", ]; +const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.String)); + +const dockerfileModule = (filePath: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const text = new TextDecoder("utf-8", { ignoreBOM: true }).decode(yield* fs.readFile(filePath)); + return `export default ${yield* encodeJsonString(text)};`; + }); + function dockerfileTextPlugin() { return { name: "dockerfile-text-loader", @@ -36,7 +47,12 @@ function dockerfileTextPlugin() { return undefined; } - return `export default ${JSON.stringify(readFileSync(filePath, "utf8"))};`; + return Effect.runPromise( + dockerfileModule(filePath).pipe( + Effect.catchTag("PlatformError", (error) => Effect.die(error.cause)), + Effect.provide(BunFileSystem.layer), + ), + ); }, }; } From f6f7859b01ce607db893537536126eb84cfbfb04 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 26 Oct 2026 14:20:28 +0000 Subject: [PATCH 21/33] feat(config): publish x-secret markers in the config JSON schema (#7330) ## Summary `secret()` and `env({ secret: true })` mark config fields with `x-secret`, but the published `config.schema.json` never carried it, because the JSON Schema generator only emits standard keywords. The only hint a field held a secret was the "do not commit" note in its description, which tools can't act on. The published CLI config schema now has `"x-secret": true` on every field the schema marks secret. That includes the `remotes.*` copies and the values of `db.vault` and `edge_runtime.secrets`. Editors ignore unknown keywords, so validation and completion behave the same, and nothing changes in how config is loaded. - `toPublishedJsonSchemaDocument` now generates both published schemas itself and lets only `x-secret` through, so internal annotations like `x-env-deferred` stay out - `project-config.schema.json` doesn't change, since it already leaves secret fields out altogether - `apps/docs/public/cli/config.schema.json` is regenerated, and the only change is the added markers ## Linked issue Closes CLI-2858 --- apps/docs/public/cli/config.schema.json | 258 ++++++++++++------ packages/config/src/base.ts | 4 +- packages/config/src/json-schema-document.ts | 21 +- .../src/json-schema-document.unit.test.ts | 52 ++++ .../src/project-config/project-schema.ts | 4 +- 5 files changed, 239 insertions(+), 100 deletions(-) diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 526ce97a0c..87a9942059 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -189,23 +189,28 @@ }, "publishable_key": { "type": "string", - "description": "Publishable key override." + "description": "Publishable key override.", + "x-secret": true }, "secret_key": { "type": "string", - "description": "Secret key override." + "description": "Secret key override.", + "x-secret": true }, "jwt_secret": { "type": "string", - "description": "JWT secret override." + "description": "JWT secret override.", + "x-secret": true }, "anon_key": { "type": "string", - "description": "Anon key override." + "description": "Anon key override.", + "x-secret": true }, "service_role_key": { "type": "string", - "description": "Service role key override." + "description": "Service role key override.", + "x-secret": true }, "rate_limit": { "type": "object", @@ -263,7 +268,8 @@ }, "secret": { "type": "string", - "description": "Secret key for the CAPTCHA provider." + "description": "Secret key for the CAPTCHA provider.", + "x-secret": true } }, "additionalProperties": false @@ -285,7 +291,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -304,7 +311,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -323,7 +331,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -342,7 +351,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -361,7 +371,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -380,7 +391,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -537,7 +549,8 @@ }, "pass": { "type": "string", - "description": "Password for authenticating with the SMTP server." + "description": "Password for authenticating with the SMTP server.", + "x-secret": true }, "admin_email": { "type": "string", @@ -686,7 +699,8 @@ "auth_token": { "type": "string", "description": "The auth token for the Twilio API.", - "examples": ["env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"] + "examples": ["env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"], + "x-secret": true } }, "additionalProperties": false @@ -709,7 +723,8 @@ }, "auth_token": { "type": "string", - "description": "The auth token for the Twilio API." + "description": "The auth token for the Twilio API.", + "x-secret": true } }, "additionalProperties": false @@ -728,7 +743,8 @@ }, "access_key": { "type": "string", - "description": "The access key for the MessageBird API." + "description": "The access key for the MessageBird API.", + "x-secret": true } }, "additionalProperties": false @@ -747,7 +763,8 @@ }, "api_key": { "type": "string", - "description": "The API key for the Textlocal API." + "description": "The API key for the Textlocal API.", + "x-secret": true } }, "additionalProperties": false @@ -770,7 +787,8 @@ }, "api_secret": { "type": "string", - "description": "The API secret for the Vonage API." + "description": "The API secret for the Vonage API.", + "x-secret": true } }, "additionalProperties": false @@ -804,7 +822,8 @@ "secret": { "type": "string", "description": "Client secret for the Apple OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -845,7 +864,8 @@ "secret": { "type": "string", "description": "Client secret for the Azure OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_AZURE_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_AZURE_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -886,7 +906,8 @@ "secret": { "type": "string", "description": "Client secret for the Bitbucket OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_BITBUCKET_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_BITBUCKET_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -927,7 +948,8 @@ "secret": { "type": "string", "description": "Client secret for the Discord OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_DISCORD_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_DISCORD_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -968,7 +990,8 @@ "secret": { "type": "string", "description": "Client secret for the Facebook OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_FACEBOOK_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_FACEBOOK_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1009,7 +1032,8 @@ "secret": { "type": "string", "description": "Client secret for the Figma OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_FIGMA_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_FIGMA_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1050,7 +1074,8 @@ "secret": { "type": "string", "description": "Client secret for the GitHub OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1091,7 +1116,8 @@ "secret": { "type": "string", "description": "Client secret for the GitLab OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_GITLAB_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_GITLAB_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1132,7 +1158,8 @@ "secret": { "type": "string", "description": "Client secret for the Google OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1173,7 +1200,8 @@ "secret": { "type": "string", "description": "Client secret for the Kakao OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_KAKAO_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_KAKAO_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1214,7 +1242,8 @@ "secret": { "type": "string", "description": "Client secret for the Keycloak OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_KEYCLOAK_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_KEYCLOAK_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1256,7 +1285,8 @@ "secret": { "type": "string", "description": "Client secret for the LinkedIn OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_LINKEDIN_OIDC_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_LINKEDIN_OIDC_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1297,7 +1327,8 @@ "secret": { "type": "string", "description": "Client secret for the Notion OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_NOTION_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_NOTION_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1338,7 +1369,8 @@ "secret": { "type": "string", "description": "Client secret for the Twitch OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_TWITCH_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_TWITCH_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1379,7 +1411,8 @@ "secret": { "type": "string", "description": "Client secret for the Twitter OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_TWITTER_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_TWITTER_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1420,7 +1453,8 @@ "secret": { "type": "string", "description": "Client secret for the X OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_X_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_X_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1461,7 +1495,8 @@ "secret": { "type": "string", "description": "Client secret for the Slack OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_SLACK_OIDC_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_SLACK_OIDC_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1502,7 +1537,8 @@ "secret": { "type": "string", "description": "Client secret for the Spotify OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_SPOTIFY_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_SPOTIFY_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1543,7 +1579,8 @@ "secret": { "type": "string", "description": "Client secret for the WorkOS OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_WORKOS_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_WORKOS_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1584,7 +1621,8 @@ "secret": { "type": "string", "description": "Client secret for the Zoom OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_ZOOM_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_ZOOM_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -1971,7 +2009,8 @@ "type": "object", "additionalProperties": { "type": "string", - "description": "Vault secret value." + "description": "Vault secret value.", + "x-secret": true }, "description": "Vault secrets." } @@ -2006,7 +2045,8 @@ "type": "object", "additionalProperties": { "type": "string", - "description": "Secret value exposed to the edge runtime." + "description": "Secret value exposed to the edge runtime.", + "x-secret": true }, "description": "Secrets exposed to the edge runtime." } @@ -2350,7 +2390,8 @@ "openai_api_key": { "type": "string", "description": "OpenAI API key to use for Supabase AI in the Supabase Studio.", - "examples": ["env(OPENAI_API_KEY)"] + "examples": ["env(OPENAI_API_KEY)"], + "x-secret": true } }, "additionalProperties": false @@ -2437,12 +2478,14 @@ "s3_access_key": { "type": "string", "description": "S3 access key.", - "examples": ["env(S3_ACCESS_KEY)"] + "examples": ["env(S3_ACCESS_KEY)"], + "x-secret": true }, "s3_secret_key": { "type": "string", "description": "S3 secret key.", - "examples": ["env(S3_SECRET_KEY)"] + "examples": ["env(S3_SECRET_KEY)"], + "x-secret": true }, "webhooks": { "type": "object", @@ -2878,23 +2921,28 @@ }, "publishable_key": { "type": "string", - "description": "Publishable key override." + "description": "Publishable key override.", + "x-secret": true }, "secret_key": { "type": "string", - "description": "Secret key override." + "description": "Secret key override.", + "x-secret": true }, "jwt_secret": { "type": "string", - "description": "JWT secret override." + "description": "JWT secret override.", + "x-secret": true }, "anon_key": { "type": "string", - "description": "Anon key override." + "description": "Anon key override.", + "x-secret": true }, "service_role_key": { "type": "string", - "description": "Service role key override." + "description": "Service role key override.", + "x-secret": true }, "rate_limit": { "type": "object", @@ -2952,7 +3000,8 @@ }, "secret": { "type": "string", - "description": "Secret key for the CAPTCHA provider." + "description": "Secret key for the CAPTCHA provider.", + "x-secret": true } }, "additionalProperties": false @@ -2974,7 +3023,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -2993,7 +3043,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -3012,7 +3063,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -3031,7 +3083,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -3050,7 +3103,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -3069,7 +3123,8 @@ }, "secrets": { "type": "string", - "description": "Secret value to pass to the function or endpoint." + "description": "Secret value to pass to the function or endpoint.", + "x-secret": true } }, "additionalProperties": false @@ -3226,7 +3281,8 @@ }, "pass": { "type": "string", - "description": "Password for authenticating with the SMTP server." + "description": "Password for authenticating with the SMTP server.", + "x-secret": true }, "admin_email": { "type": "string", @@ -3375,7 +3431,8 @@ "auth_token": { "type": "string", "description": "The auth token for the Twilio API.", - "examples": ["env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"] + "examples": ["env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"], + "x-secret": true } }, "additionalProperties": false @@ -3398,7 +3455,8 @@ }, "auth_token": { "type": "string", - "description": "The auth token for the Twilio API." + "description": "The auth token for the Twilio API.", + "x-secret": true } }, "additionalProperties": false @@ -3417,7 +3475,8 @@ }, "access_key": { "type": "string", - "description": "The access key for the MessageBird API." + "description": "The access key for the MessageBird API.", + "x-secret": true } }, "additionalProperties": false @@ -3436,7 +3495,8 @@ }, "api_key": { "type": "string", - "description": "The API key for the Textlocal API." + "description": "The API key for the Textlocal API.", + "x-secret": true } }, "additionalProperties": false @@ -3459,7 +3519,8 @@ }, "api_secret": { "type": "string", - "description": "The API secret for the Vonage API." + "description": "The API secret for the Vonage API.", + "x-secret": true } }, "additionalProperties": false @@ -3493,7 +3554,8 @@ "secret": { "type": "string", "description": "Client secret for the Apple OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3534,7 +3596,8 @@ "secret": { "type": "string", "description": "Client secret for the Azure OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_AZURE_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_AZURE_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3575,7 +3638,8 @@ "secret": { "type": "string", "description": "Client secret for the Bitbucket OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_BITBUCKET_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_BITBUCKET_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3616,7 +3680,8 @@ "secret": { "type": "string", "description": "Client secret for the Discord OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_DISCORD_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_DISCORD_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3657,7 +3722,8 @@ "secret": { "type": "string", "description": "Client secret for the Facebook OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_FACEBOOK_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_FACEBOOK_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3698,7 +3764,8 @@ "secret": { "type": "string", "description": "Client secret for the Figma OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_FIGMA_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_FIGMA_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3739,7 +3806,8 @@ "secret": { "type": "string", "description": "Client secret for the GitHub OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3780,7 +3848,8 @@ "secret": { "type": "string", "description": "Client secret for the GitLab OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_GITLAB_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_GITLAB_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3821,7 +3890,8 @@ "secret": { "type": "string", "description": "Client secret for the Google OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3862,7 +3932,8 @@ "secret": { "type": "string", "description": "Client secret for the Kakao OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_KAKAO_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_KAKAO_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3903,7 +3974,8 @@ "secret": { "type": "string", "description": "Client secret for the Keycloak OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_KEYCLOAK_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_KEYCLOAK_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3945,7 +4017,8 @@ "secret": { "type": "string", "description": "Client secret for the LinkedIn OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_LINKEDIN_OIDC_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_LINKEDIN_OIDC_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -3986,7 +4059,8 @@ "secret": { "type": "string", "description": "Client secret for the Notion OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_NOTION_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_NOTION_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -4027,7 +4101,8 @@ "secret": { "type": "string", "description": "Client secret for the Twitch OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_TWITCH_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_TWITCH_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -4068,7 +4143,8 @@ "secret": { "type": "string", "description": "Client secret for the Twitter OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_TWITTER_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_TWITTER_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -4109,7 +4185,8 @@ "secret": { "type": "string", "description": "Client secret for the X OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_X_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_X_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -4150,7 +4227,8 @@ "secret": { "type": "string", "description": "Client secret for the Slack OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_SLACK_OIDC_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_SLACK_OIDC_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -4191,7 +4269,8 @@ "secret": { "type": "string", "description": "Client secret for the Spotify OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_SPOTIFY_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_SPOTIFY_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -4232,7 +4311,8 @@ "secret": { "type": "string", "description": "Client secret for the WorkOS OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_WORKOS_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_WORKOS_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -4273,7 +4353,8 @@ "secret": { "type": "string", "description": "Client secret for the Zoom OAuth provider.\n\nDO NOT commit your OAuth provider secret to git. Use environment variable substitution instead.", - "examples": ["env(SUPABASE_AUTH_EXTERNAL_ZOOM_SECRET)"] + "examples": ["env(SUPABASE_AUTH_EXTERNAL_ZOOM_SECRET)"], + "x-secret": true }, "url": { "type": "string", @@ -4660,7 +4741,8 @@ "type": "object", "additionalProperties": { "type": "string", - "description": "Vault secret value." + "description": "Vault secret value.", + "x-secret": true }, "description": "Vault secrets." } @@ -4695,7 +4777,8 @@ "type": "object", "additionalProperties": { "type": "string", - "description": "Secret value exposed to the edge runtime." + "description": "Secret value exposed to the edge runtime.", + "x-secret": true }, "description": "Secrets exposed to the edge runtime." } @@ -5039,7 +5122,8 @@ "openai_api_key": { "type": "string", "description": "OpenAI API key to use for Supabase AI in the Supabase Studio.", - "examples": ["env(OPENAI_API_KEY)"] + "examples": ["env(OPENAI_API_KEY)"], + "x-secret": true } }, "additionalProperties": false @@ -5126,12 +5210,14 @@ "s3_access_key": { "type": "string", "description": "S3 access key.", - "examples": ["env(S3_ACCESS_KEY)"] + "examples": ["env(S3_ACCESS_KEY)"], + "x-secret": true }, "s3_secret_key": { "type": "string", "description": "S3 secret key.", - "examples": ["env(S3_SECRET_KEY)"] + "examples": ["env(S3_SECRET_KEY)"], + "x-secret": true }, "webhooks": { "type": "object", diff --git a/packages/config/src/base.ts b/packages/config/src/base.ts index 461930b8ef..143762fd7f 100644 --- a/packages/config/src/base.ts +++ b/packages/config/src/base.ts @@ -83,9 +83,7 @@ export const CliConfigSchema = Schema.Struct({ }); export function toCliConfigJsonSchema() { - return toPublishedJsonSchemaDocument( - Schema.toJsonSchemaDocument(CliConfigSchema, { onExcessProperty: "error" }), - ); + return toPublishedJsonSchemaDocument(CliConfigSchema, { onExcessProperty: "error" }); } export type CliConfig = typeof CliConfigSchema.Type; diff --git a/packages/config/src/json-schema-document.ts b/packages/config/src/json-schema-document.ts index 3b996c03a9..2fbc1cbe66 100644 --- a/packages/config/src/json-schema-document.ts +++ b/packages/config/src/json-schema-document.ts @@ -1,13 +1,18 @@ +import { Schema } from "effect"; + /** - * Assembles a published JSON Schema document from a generated schema and its definitions. - * Pattern-keyed records stay open: the generator now writes a boolean `additionalProperties` - * on them (effect 4.0) and has no per-schema setting, while the published documents never - * carried one there. Structs keep theirs. + * Renders a published JSON Schema document, carrying the `x-secret` marker through and keeping + * pattern-keyed records open: the generator writes a boolean `additionalProperties` on them + * (effect 4.0) with no per-schema setting, while the published documents never carried one there. */ -export function toPublishedJsonSchemaDocument(document: { - readonly schema: Record; - readonly definitions: Record; -}) { +export function toPublishedJsonSchemaDocument( + schema: Schema.Constraint, + options: { readonly onExcessProperty: "ignore" | "error" }, +) { + const document = Schema.toJsonSchemaDocument(schema, { + onExcessProperty: options.onExcessProperty, + includeAnnotationKey: (key) => key === "x-secret", + }); return { $schema: "https://json-schema.org/draft/2020-12/schema", ...openPatternKeyedRecords(document.schema), diff --git a/packages/config/src/json-schema-document.unit.test.ts b/packages/config/src/json-schema-document.unit.test.ts index efa0e87e9c..99eba9d5ee 100644 --- a/packages/config/src/json-schema-document.unit.test.ts +++ b/packages/config/src/json-schema-document.unit.test.ts @@ -1,5 +1,9 @@ +import { Schema } from "effect"; import { describe, expect, test } from "vitest"; import { toCliConfigJsonSchema } from "./base.ts"; +import { toPublishedJsonSchemaDocument } from "./json-schema-document.ts"; +import { env } from "./lib/env.ts"; +import { secretPathPatterns } from "./lib/secret-paths.ts"; import { toProjectConfigJsonSchema } from "./project-config/project-schema.ts"; type Node = Record; @@ -10,6 +14,34 @@ const at = (node: unknown, ...path: ReadonlyArray): Node => { return current as Node; }; +const isNode = (value: unknown): value is Node => + typeof value === "object" && value !== null && !Array.isArray(value); + +const collectSecretPaths = (document: unknown): Array => { + const found = new Set(); + const visit = (node: unknown, path: ReadonlyArray): void => { + if (Array.isArray(node)) { + for (const item of node) visit(item, path); + return; + } + if (!isNode(node)) return; + if (node["x-secret"] === true) found.add(path.join(".")); + for (const [keyword, value] of Object.entries(node)) { + if (keyword === "properties" && isNode(value)) { + for (const [name, child] of Object.entries(value)) visit(child, [...path, name]); + } else if (keyword === "patternProperties" && isNode(value)) { + for (const child of Object.values(value)) visit(child, [...path, "*"]); + } else if (keyword === "additionalProperties") { + visit(value, [...path, "*"]); + } else if (keyword === "anyOf" || keyword === "oneOf" || keyword === "allOf") { + visit(value, path); + } + } + }; + visit(document, []); + return [...found].sort(); +}; + describe("published JSON Schema documents", () => { test("pattern-keyed records stay open while structs stay closed", () => { const cli = toCliConfigJsonSchema(); @@ -37,4 +69,24 @@ describe("published JSON Schema documents", () => { anyOf: [{ type: "object" }, { type: "array" }], }); }); + + test("the CLI schema marks exactly the secret paths the schema declares", () => { + const declared = secretPathPatterns.map((pattern) => pattern.join(".")).sort(); + expect(declared).not.toHaveLength(0); + expect(collectSecretPaths(toCliConfigJsonSchema())).toEqual(declared); + }); + + test("the project config schema carries no secret markers", () => { + expect(JSON.stringify(toProjectConfigJsonSchema())).not.toContain('"x-secret"'); + }); + + test("custom annotations other than x-secret are not published", () => { + const deferredSecret = toPublishedJsonSchemaDocument( + Schema.Struct({ token: env({ secret: true }) }), + { onExcessProperty: "error" }, + ); + expect(collectSecretPaths(deferredSecret)).toEqual(["token"]); + expect(JSON.stringify(deferredSecret)).not.toContain("x-env-deferred"); + expect(JSON.stringify(toCliConfigJsonSchema())).not.toContain("x-env-deferred"); + }); }); diff --git a/packages/config/src/project-config/project-schema.ts b/packages/config/src/project-config/project-schema.ts index 567469d90e..029291db74 100644 --- a/packages/config/src/project-config/project-schema.ts +++ b/packages/config/src/project-config/project-schema.ts @@ -139,7 +139,5 @@ export const ProjectConfigSchema: StandardSchemaV1< /** JSON Schema (draft 2020-12) rendering of {@link ProjectConfigSchema}. */ export function toProjectConfigJsonSchema() { - return toPublishedJsonSchemaDocument( - Schema.toJsonSchemaDocument(ProjectConfigSchema, { onExcessProperty: "ignore" }), - ); + return toPublishedJsonSchemaDocument(ProjectConfigSchema, { onExcessProperty: "ignore" }); } From 3e91ac63a731542b46be9bdf525d010a6a4ebd69 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:24:59 +0000 Subject: [PATCH 22/33] chore(stack): bump realtime to v2.148.0-r0 (#7332) This bumps realtime from v2.147.4-r0 to v2.148.0-r0. https://github.com/supabase/slim-services/releases/tag/realtime-v2.148.0-r0 Planned after supabase/slim-services published v2.148.0-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 12dae979be..2360328102 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.147.4 AS realtime +FROM supabase/realtime:v2.148.0 AS realtime FROM supabase/storage-api:v1.84.23 AS storage diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 12dae979be..2360328102 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.147.4 AS realtime +FROM supabase/realtime:v2.148.0 AS realtime FROM supabase/storage-api:v1.84.23 AS storage diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index f242fe917d..ffe8ee559f 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -317,23 +317,23 @@ const definitions: Readonly> = { realtime: definition( "realtime", { - upstreamVersion: "v2.147.4", + upstreamVersion: "v2.148.0", revision: 0, image: - "ghcr.io/supabase/cli/realtime:v2.147.4-r0@sha256:70e5f3099579b5e2ee422152ab3d30dd74e3205370edbbf9927a50c2d06749a7", - upstreamImage: "supabase/realtime:v2.147.4", + "ghcr.io/supabase/cli/realtime:v2.148.0-r0@sha256:1d37640784ead01cd26275971e10a4dc951986a55e52e1ad2d268e5f905047bc", + upstreamImage: "supabase/realtime:v2.148.0", natives: { "darwin-arm64": { - archive: "f692c6c64ac10e94e2b2ac911b57739207a63109359078c8755f77b0f2c74513", - manifest: "d989b06735a2e72b5a40a3e08714577ba139892e68f20602aeecae53b22ffd42", + archive: "c05e41edb76a81eea730d52d1e2a6c7a4d65e5da1b352eaf2039237f1e012976", + manifest: "d6cf5ad57d5b044d28e33ebc9d072a4e8a5ce7514a4df951524dd65b509bcaa7", }, "linux-amd64": { - archive: "65d92c29ad286cdb6218a9bf26541ac48a1fbf5726cc31af0a06e895d5086558", - manifest: "8b2cd04ed1381a38dd578a42c200698c7aff190f5e554f2ea7d79da45bad2194", + archive: "0f06596745415e3a3eceaea1309ab10af59d6ff656e6be2c9f47ce8916179504", + manifest: "3362ee4a27062cb584e991563b8c231c5852ca0ff534c6c5ed11b252d4a4ec88", }, "linux-arm64": { - archive: "a3e6fab857e1d51a3cc0f9a088eb0b2e4a90b300269346c4d26f36d45b495c37", - manifest: "46624b79879156579886bc1508e10ff6f414e6fce731681a0f906a7b0875dcf3", + archive: "3abf8cb052f0ad9f16276a9be8f9af4e51eca31c9a4e8bd666938dca4e6cc1a5", + manifest: "8e652bb45eb7f67da4071377a5efdc3aa833a2a4655a77f1336036fcaf217634", }, }, }, From b113cd31c49376498d378e6a9d4913a380050ef4 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:42:51 +0000 Subject: [PATCH 23/33] chore(stack): bump storage to v1.84.25-r0 (#7335) This bumps storage from v1.84.23-r0 to v1.84.25-r0. https://github.com/supabase/slim-services/releases/tag/storage-v1.84.25-r0 Planned after supabase/slim-services published v1.84.25-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 2360328102..2045fdc1de 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.148.0 AS realtime -FROM supabase/storage-api:v1.84.23 AS storage +FROM supabase/storage-api:v1.84.25 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 2360328102..2045fdc1de 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.148.0 AS realtime -FROM supabase/storage-api:v1.84.23 AS storage +FROM supabase/storage-api:v1.84.25 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index ffe8ee559f..bd3e3dfb96 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -343,23 +343,23 @@ const definitions: Readonly> = { storage: definition( "storage", { - upstreamVersion: "v1.84.23", + upstreamVersion: "v1.84.25", revision: 0, image: - "ghcr.io/supabase/cli/storage:v1.84.23-r0@sha256:069a20dea29b554e77f49b1202401f533d3d00b3d067460a06d9a8ecc8c9d936", - upstreamImage: "supabase/storage-api:v1.84.23", + "ghcr.io/supabase/cli/storage:v1.84.25-r0@sha256:c28d1ad8c63ce0b487754227f1eb97ca83dc2ccab58c34050fcc46f4ec2198d7", + upstreamImage: "supabase/storage-api:v1.84.25", natives: { "darwin-arm64": { - archive: "cb37256862e36a349d42e70a0c72e440d4bd115151c84fd86623b7de099f09dc", - manifest: "3195d4c7a04d62e72fa7d7c47ef7957cbba0924fdcbe6f3aec5aa6799a6deeda", + archive: "b7709c1f9d8f494e774313d82651d218cf7cb6833c7598823230289dcfe9a03f", + manifest: "bad18030ee947d49dd1c21350bea7a8f1d58370a6f1a837246266e7f254cf861", }, "linux-amd64": { - archive: "4f266da712632ca53ee9644e4174c178dde1a7bd6f466893ac784a82a27e5579", - manifest: "df83e25170e057fca96b249546f2b198f7caec64531a557d1aafd283ec2059bc", + archive: "07bb85a098b3712947759dbeffd37323c0a1e1856e46313c613d2e0e8068170f", + manifest: "aac9b81078b7f45588a9fb5ed4e3f473b4fc3c95d844258f367df06279e14bf7", }, "linux-arm64": { - archive: "747169a52dab3acda568dccb0559fb27ae249bdb08b2361c477f4ba27626a4b6", - manifest: "ddb9c6dd2637e7415c87c7db604d8adadf8e65bd800c254c04256705f5750dc9", + archive: "58bbaf780f131d4d59da13e25360881846d2dbdca6dbca1973019793192b84bd", + manifest: "47a61a0c93b977ee846f5bedf6969c85af546cc9f485cc03dd7cd441a12c0dc1", }, }, }, From cef702006780be58cc9c981ca531876f02291cac Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:56:24 +0000 Subject: [PATCH 24/33] chore(stack): bump realtime to v2.148.1-r0 (#7337) This bumps realtime from v2.148.0-r0 to v2.148.1-r0. https://github.com/supabase/slim-services/releases/tag/realtime-v2.148.1-r0 Planned after supabase/slim-services published v2.148.1-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 2045fdc1de..ee746773c2 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.148.0 AS realtime +FROM supabase/realtime:v2.148.1 AS realtime FROM supabase/storage-api:v1.84.25 AS storage diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 2045fdc1de..ee746773c2 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.148.0 AS realtime +FROM supabase/realtime:v2.148.1 AS realtime FROM supabase/storage-api:v1.84.25 AS storage diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index bd3e3dfb96..0baac33ece 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -317,23 +317,23 @@ const definitions: Readonly> = { realtime: definition( "realtime", { - upstreamVersion: "v2.148.0", + upstreamVersion: "v2.148.1", revision: 0, image: - "ghcr.io/supabase/cli/realtime:v2.148.0-r0@sha256:1d37640784ead01cd26275971e10a4dc951986a55e52e1ad2d268e5f905047bc", - upstreamImage: "supabase/realtime:v2.148.0", + "ghcr.io/supabase/cli/realtime:v2.148.1-r0@sha256:1cfe6e49fbccf0c72e0f616ef6e31f5a238aa872dfac21f3405bf73da4a89c35", + upstreamImage: "supabase/realtime:v2.148.1", natives: { "darwin-arm64": { - archive: "c05e41edb76a81eea730d52d1e2a6c7a4d65e5da1b352eaf2039237f1e012976", - manifest: "d6cf5ad57d5b044d28e33ebc9d072a4e8a5ce7514a4df951524dd65b509bcaa7", + archive: "e19b4ef652908964003a94339e7565a36cab3a6a89e73ec4b44df5fba4aaeec7", + manifest: "6fd1b8dbfee15b621ae4c8724d80bed4e5db60eb06bb3cfadc48abbff3daba1a", }, "linux-amd64": { - archive: "0f06596745415e3a3eceaea1309ab10af59d6ff656e6be2c9f47ce8916179504", - manifest: "3362ee4a27062cb584e991563b8c231c5852ca0ff534c6c5ed11b252d4a4ec88", + archive: "dd021342498292998d797af9d40a7b9c89e5d9c8d315538be77a3c4c18cd36e0", + manifest: "97cfd10899e89f70053e13fa1b41062059718e8a64fe40ca5adfd721ce97b5f2", }, "linux-arm64": { - archive: "3abf8cb052f0ad9f16276a9be8f9af4e51eca31c9a4e8bd666938dca4e6cc1a5", - manifest: "8e652bb45eb7f67da4071377a5efdc3aa833a2a4655a77f1336036fcaf217634", + archive: "f74456c289d5bc719639775d08627b8624b8d1ad2c6323a2e4d676625921e1ef", + manifest: "0a3dff09790b0d75c42779bfc60ed53c3ba2aff02810ce6831993a8edf79ea45", }, }, }, From 3584594296ddd7e2132628952127252dbf4b842b Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:47:07 +0000 Subject: [PATCH 25/33] chore(stack): bump realtime to v2.148.2-r0 (#7341) This bumps realtime from v2.148.1-r0 to v2.148.2-r0. https://github.com/supabase/slim-services/releases/tag/realtime-v2.148.2-r0 Planned after supabase/slim-services published v2.148.2-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index ee746773c2..41e8d95fb7 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.148.1 AS realtime +FROM supabase/realtime:v2.148.2 AS realtime FROM supabase/storage-api:v1.84.25 AS storage diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index ee746773c2..41e8d95fb7 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -33,7 +33,7 @@ FROM supabase/supavisor:2.9.13 AS supavisor FROM supabase/gotrue:v2.197.0 AS gotrue -FROM supabase/realtime:v2.148.1 AS realtime +FROM supabase/realtime:v2.148.2 AS realtime FROM supabase/storage-api:v1.84.25 AS storage diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 0baac33ece..b32b08c0c9 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -317,23 +317,23 @@ const definitions: Readonly> = { realtime: definition( "realtime", { - upstreamVersion: "v2.148.1", + upstreamVersion: "v2.148.2", revision: 0, image: - "ghcr.io/supabase/cli/realtime:v2.148.1-r0@sha256:1cfe6e49fbccf0c72e0f616ef6e31f5a238aa872dfac21f3405bf73da4a89c35", - upstreamImage: "supabase/realtime:v2.148.1", + "ghcr.io/supabase/cli/realtime:v2.148.2-r0@sha256:db3c83c0f0529150442de195df939cf7030371d62f27983f516d9af86326dd1f", + upstreamImage: "supabase/realtime:v2.148.2", natives: { "darwin-arm64": { - archive: "e19b4ef652908964003a94339e7565a36cab3a6a89e73ec4b44df5fba4aaeec7", - manifest: "6fd1b8dbfee15b621ae4c8724d80bed4e5db60eb06bb3cfadc48abbff3daba1a", + archive: "be25a92dca294827c453c1f3a84ed79fa005ba948f662da0c53e3074facf2194", + manifest: "7d2cbbafdbcb61a2cfff187478884ba16ab2f13ceae9d7fc7ca9da914686c73d", }, "linux-amd64": { - archive: "dd021342498292998d797af9d40a7b9c89e5d9c8d315538be77a3c4c18cd36e0", - manifest: "97cfd10899e89f70053e13fa1b41062059718e8a64fe40ca5adfd721ce97b5f2", + archive: "5e64533bc7ad60b73adec46998022e86c4a8e46995776110c78bd45253fba498", + manifest: "b14943ad28df9dbfe0ed8f020ccce8d65fedbff33a5e094872ac45943a4b6c1d", }, "linux-arm64": { - archive: "f74456c289d5bc719639775d08627b8624b8d1ad2c6323a2e4d676625921e1ef", - manifest: "0a3dff09790b0d75c42779bfc60ed53c3ba2aff02810ce6831993a8edf79ea45", + archive: "5bc69fd58028508527c93e2ef6eeb548dcabda67022c7eaaab14f8dc4d544ded", + manifest: "051afda1d3145f7c65ac85c9e3c5dff1bf3764aa01b5227c77fb41be84aa7675", }, }, }, From 071b58f63c477516462727f875e14fdd901c0a52 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:38:09 +0000 Subject: [PATCH 26/33] ci(cli): unblock the go test job (#7343) ## TL;DR unblocks go ci `Test`, red since #7287, by listing the nested `pkg` module with `go list -C pkg` so pkg coverage returns and the coveralls total drops ## ref:
as seen in: image
--- .github/workflows/cli-go-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cli-go-ci.yml b/.github/workflows/cli-go-ci.yml index 97ee9dc277..6ecd297cf5 100644 --- a/.github/workflows/cli-go-ci.yml +++ b/.github/workflows/cli-go-ci.yml @@ -32,7 +32,7 @@ jobs: - uses: t1m0thyj/unlock-keyring@cbcf205c879ebd86add70bab3a6abfcce59a5cae # v1.2.0 - run: | set -o pipefail - pkgs=$(go list ./pkg/... | grep -Ev 'pkg/api' | paste -sd ',' -) + pkgs=$(go list -C pkg ./... | grep -Ev 'pkg/api' | paste -sd ',' -) go tool gotestsum -- -race -v -count=1 ./... \ -coverpkg="./cmd/...,./internal/...,${pkgs}" -coverprofile=coverage.out From af99918e23cf427e89801d9a5ab5ff23b4e7d6d1 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:19:32 +0000 Subject: [PATCH 27/33] refactor(cli): cover `apps/cli/scripts` with effect lint (CLI-2853) (#7336) ## TL;DR brings `apps/cli/scripts` under the effect lint, so the allow list collapses into one `!apps/cli/**` entry ## whats introduced? - `apps/cli/scripts/**` joins the allow list, and the `src`, `tests` and `vitest.config.ts` entries collapse into `!apps/cli/**` - the build, release, publish and docs scripts run as effect programs on `FileSystem`, `HttpClient` and `Config`, with the same arguments, output, error messages, generated files and exit codes - a failed run still crashes with the original error, so malformed json keeps its native `SyntaxError`, and a signal during `refresh-e2e-timings` still waits for its cleanup - the scripts tests move to `it.effect` and `it.live` with the same names and assertions ## ref - closes: CLI-2853 - closes: CLI-2851 --- .oxlintrc.effect.json | 4 +- apps/cli/scripts/apply-release-notes.ts | 55 +- apps/cli/scripts/backfill-release-notes.ts | 497 +++++++----- apps/cli/scripts/bench-cli.ts | 24 +- .../scripts/build-binary.integration.test.ts | 179 +++-- apps/cli/scripts/build-binary.ts | 4 +- apps/cli/scripts/build.ts | 715 ++++++++++-------- apps/cli/scripts/compile-options.ts | 9 +- apps/cli/scripts/detect-unmirrored-images.ts | 287 +++++-- .../detect-unmirrored-images.unit.test.ts | 121 +-- apps/cli/scripts/generate-docs-spec.ts | 6 +- apps/cli/scripts/generate-docs.ts | 232 +++--- apps/cli/scripts/propose-release-notes.ts | 278 ++++--- .../cli/scripts/publish-docs-spec.e2e.test.ts | 4 +- apps/cli/scripts/publish-docs-spec.ts | 304 +++++--- apps/cli/scripts/publish.ts | 251 +++--- apps/cli/scripts/refresh-e2e-timings.ts | 319 ++++++-- ...nder-service-dockerfile.rules.unit.test.ts | 8 +- apps/cli/scripts/render-service-dockerfile.ts | 48 +- .../render-service-dockerfile.unit.test.ts | 17 +- .../sweep-live-projects.integration.test.ts | 354 +++++---- apps/cli/scripts/sync-versions.ts | 57 +- apps/cli/scripts/trace-report.ts | 13 +- apps/cli/scripts/trace-report.unit.test.ts | 24 +- apps/cli/scripts/update-homebrew.ts | 211 +++--- apps/cli/scripts/update-scoop.ts | 247 +++--- .../upload-release-assets.integration.test.ts | 309 ++++---- apps/cli/scripts/upload-release-assets.ts | 179 +++-- .../upload-release-assets.unit.test.ts | 306 ++++---- 29 files changed, 3062 insertions(+), 2000 deletions(-) diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index d7582143a6..6964884c1d 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -9,9 +9,7 @@ "!packages/api/**", "!packages/config/**", "!packages/stack/**", - "!apps/cli/src/**", - "!apps/cli/tests/**", - "!apps/cli/vitest.config.ts", + "!apps/cli/**", // Last match wins across the whole list: keep bare re-exclusions after every // `!` entry that would otherwise re-include them. "apps/cli/src/shared/cli/bin.ts", diff --git a/apps/cli/scripts/apply-release-notes.ts b/apps/cli/scripts/apply-release-notes.ts index 9027208231..c63c90effa 100644 --- a/apps/cli/scripts/apply-release-notes.ts +++ b/apps/cli/scripts/apply-release-notes.ts @@ -5,12 +5,19 @@ // // Usage: // bun apps/cli/scripts/apply-release-notes.ts --tag v2.101.0 +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -import { existsSync } from "node:fs"; -import path from "node:path"; +import { Console, Data, Effect, FileSystem, Layer, Path } from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; +class ShellCommandFailed extends Data.TaggedError("ShellCommandFailed")<{ + readonly cause: unknown; +}> {} + +const shell =
(command: () => Promise) => + Effect.tryPromise({ try: command, catch: (cause) => new ShellCommandFailed({ cause }) }); + const { values } = parseArgs({ options: { tag: { type: "string" }, @@ -18,20 +25,36 @@ const { values } = parseArgs({ strict: true, }); -const tag = values.tag; -if (!tag) { - console.error("--tag is required (e.g. --tag v2.101.0)"); - process.exit(2); -} -const version = tag.replace(/^v/, ""); +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + + const tag = values.tag; + if (!tag) { + yield* Console.error("--tag is required (e.g. --tag v2.101.0)"); + return yield* Effect.sync(() => process.exit(2)); + } + const version = tag.replace(/^v/, ""); + + const repoRoot = (yield* shell(() => $`git rev-parse --show-toplevel`.text())).trim(); + const notesPath = path.join(repoRoot, "release-notes", `v${version}.md`); + if (!(yield* fs.exists(notesPath).pipe(Effect.orElseSucceed(() => false)))) { + yield* Console.error(`No notes file at ${path.relative(repoRoot, notesPath)}`); + return yield* Effect.sync(() => process.exit(1)); + } + + yield* Console.error(`==> Updating GitHub Release body for ${tag}`); + yield* shell(() => $`gh release edit ${tag} --notes-file ${notesPath}`.cwd(repoRoot)); + yield* Console.error(`==> Done`); +}); -const repoRoot = (await $`git rev-parse --show-toplevel`.text()).trim(); -const notesPath = path.join(repoRoot, "release-notes", `v${version}.md`); -if (!existsSync(notesPath)) { - console.error(`No notes file at ${path.relative(repoRoot, notesPath)}`); - process.exit(1); +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + return error.cause !== undefined ? error.cause : new Error(error.message); } -console.error(`==> Updating GitHub Release body for ${tag}`); -await $`gh release edit ${tag} --notes-file ${notesPath}`.cwd(repoRoot); -console.error(`==> Done`); +await Effect.runPromise( + main.pipe( + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + Effect.catch((error) => Effect.die(thrownValue(error))), + ), +); diff --git a/apps/cli/scripts/backfill-release-notes.ts b/apps/cli/scripts/backfill-release-notes.ts index e578450a53..e5b81aed56 100644 --- a/apps/cli/scripts/backfill-release-notes.ts +++ b/apps/cli/scripts/backfill-release-notes.ts @@ -18,15 +18,39 @@ // --tag Required. Release tag to refresh (e.g. v2.99.0-beta.1). // --apply Update the GitHub Release body via `gh release edit`. // Without it, raw markdown notes are printed to stdout. +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { Console, Data, Effect, FileSystem, Layer, Path, Schema } from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; import semanticRelease from "semantic-release"; import { channelForTag, releaseBranchForTag } from "./release-channels.ts"; +class JsonParseFailed extends Data.TaggedError("JsonParseFailed")<{ readonly cause: unknown }> {} + +class ShellCommandFailed extends Data.TaggedError("ShellCommandFailed")<{ + readonly cause: unknown; +}> {} + +class SemanticReleaseFailed extends Data.TaggedError("SemanticReleaseFailed")<{ + readonly cause: unknown; +}> {} + +class LocalReleasePluginOutsideCli extends Data.TaggedError("LocalReleasePluginOutsideCli")<{ + readonly message: string; +}> {} + +const shell = (command: () => Promise) => + Effect.tryPromise({ try: command, catch: (cause) => new ShellCommandFailed({ cause }) }); + +const utf8 = new TextDecoder("utf-8", { ignoreBOM: true }); +/* Native `JSON.parse`, so malformed JSON fails with its own `SyntaxError`. */ +const parseJson = (text: string) => JSON.parse(text); +const decodeJson = (text: string) => + Effect.try({ try: () => parseJson(text), catch: (cause) => new JsonParseFailed({ cause }) }); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); +const encodePackageJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 })); + const { values } = parseArgs({ options: { tag: { type: "string" }, @@ -35,232 +59,287 @@ const { values } = parseArgs({ strict: true, }); -const tag = values.tag; -if (!tag) { - console.error("--tag is required (e.g. --tag v2.99.0-beta.1)"); - process.exit(2); -} -const apply = values.apply ?? false; - -const repoRoot = (await $`git rev-parse --show-toplevel`.text()).trim(); -const cliDir = path.join(repoRoot, "apps/cli"); - -const rootPkg = JSON.parse(await readFile(path.join(cliDir, "package.json"), "utf8")); - function releasePluginSpecifier(plugin: unknown): string | undefined { if (typeof plugin === "string") return plugin; if (Array.isArray(plugin) && typeof plugin[0] === "string") return plugin[0]; return undefined; } -async function copyLocalReleasePlugins(clone: string): Promise { - const plugins = Array.isArray(rootPkg.release?.plugins) ? rootPkg.release.plugins : []; - const historicalCliDir = path.join(clone, "apps/cli"); - - for (const plugin of plugins) { - const specifier = releasePluginSpecifier(plugin); - if (!specifier || path.isAbsolute(specifier) || !specifier.startsWith(".")) continue; - - const sourcePath = path.resolve(cliDir, specifier); - const relativePath = path.relative(cliDir, sourcePath); - if ( - relativePath === ".." || - relativePath.startsWith(`..${path.sep}`) || - path.isAbsolute(relativePath) - ) { - throw new Error(`Local release plugin must stay inside apps/cli: ${specifier}`); - } +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; - const destinationPath = path.join(historicalCliDir, relativePath); - await mkdir(path.dirname(destinationPath), { recursive: true }); - await copyFile(sourcePath, destinationPath); + const tag = values.tag; + if (!tag) { + yield* Console.error("--tag is required (e.g. --tag v2.99.0-beta.1)"); + return yield* Effect.sync(() => process.exit(2)); } -} + const apply = values.apply ?? false; -const repoField = rootPkg.repository?.url ?? rootPkg.repository ?? ""; -const repoUrl = `${String(repoField) - .replace(/^git\+/, "") - .replace(/\.git$/, "") - .replace(/\/$/, "")}.git`; -if (!repoUrl.startsWith("http")) { - console.error(`Could not derive repository URL from apps/cli/package.json (got: ${repoUrl})`); - process.exit(1); -} + const repoRoot = (yield* shell(() => $`git rev-parse --show-toplevel`.text())).trim(); + const cliDir = path.join(repoRoot, "apps/cli"); -const tagCheck = await $`git rev-parse -q --verify refs/tags/${tag}` - .cwd(repoRoot) - .nothrow() - .quiet(); -if (tagCheck.exitCode !== 0) { - console.error(`Tag ${tag} not found locally. Try: git fetch --tags origin`); - process.exit(1); -} + const rootPkg = yield* decodeJson( + utf8.decode(yield* fs.readFile(path.join(cliDir, "package.json"))), + ); -const work = await mkdtemp(path.join(tmpdir(), "backfill-release-notes.")); -const clone = path.join(work, "repo"); - -try { - console.error(`==> Cloning ${repoRoot} -> ${clone}`); - await $`git clone --quiet --no-local ${repoRoot} ${clone}`; - - // `git notes add` (used below to seed channel notes) requires a committer - // identity, which CI runners don't ship in ~/.gitconfig. - await $`git -C ${clone} config --local user.email backfill-release-notes@supabase.local`; - await $`git -C ${clone} config --local user.name backfill-release-notes`; - // A signing key inherited from a user's global config would also make - // `git notes add` fail; the temp clone never publishes anything, so disable it. - await $`git -C ${clone} config --local commit.gpgsign false`; - await $`git -C ${clone} config --local tag.gpgsign false`; - - const originUrl = (await $`git -C ${repoRoot} remote get-url origin`.text()).trim(); - - // Notes refs aren't fetched by `git clone`. Pull from the source repo first - // (network-free), then from origin so even tags whose notes haven't been - // sync'd locally yet are available. - await $`git -C ${clone} fetch --no-tags --quiet ${repoRoot} +refs/notes/*:refs/notes/*` - .nothrow() - .quiet(); - await $`git -C ${clone} fetch --no-tags --quiet ${originUrl} +refs/notes/*:refs/notes/*` - .nothrow() - .quiet(); - // One fetch per branch: git aborts a multi-ref fetch when any named ref is missing, and `next` - // may not exist. The `v*.x` glob matches zero or more branches without failing. - for (const branchSpec of ["main", "develop", "next", "v*.x"]) { - await $`git -C ${clone} fetch --no-tags --quiet ${originUrl} +refs/heads/${branchSpec}:refs/remotes/origin/${branchSpec}` - .nothrow() - .quiet(); - } + const copyLocalReleasePlugins = Effect.fnUntraced(function* (clone: string) { + const plugins = Array.isArray(rootPkg.release?.plugins) ? rootPkg.release.plugins : []; + const historicalCliDir = path.join(clone, "apps/cli"); - const sha = (await $`git -C ${clone} rev-list -n 1 ${tag}`.text()).trim(); - const onMain = - ( - await $`git -C ${clone} merge-base --is-ancestor ${sha} refs/remotes/origin/main` - .nothrow() - .quiet() - ).exitCode === 0; - const branch = releaseBranchForTag(tag, onMain); - if ( - branch !== "main" && - branch !== "develop" && - branch !== "next" && - (await $`git -C ${clone} rev-parse --verify -q refs/remotes/origin/${branch}`.nothrow().quiet()) - .exitCode !== 0 - ) { - console.error(`${tag} belongs to ${branch}, but origin/${branch} does not exist.`); - process.exit(1); - } - // Delete the target tag and any other local tag pointing at the same commit: - // when a stable and a beta share a commit, semantic-release picks the - // higher-semver one as lastRelease (which becomes HEAD itself, leaving "no - // release"). Dropping the co-incident tags falls back to the genuine prior release. - const coincidentTagsOut = await $`git -C ${clone} tag --points-at ${sha}`.text(); - const coincidentTags = coincidentTagsOut - .split("\n") - .map((t) => t.trim()) - .filter(Boolean); - for (const ct of coincidentTags) { - await $`git -C ${clone} tag -d ${ct}`.quiet().nothrow(); - } - await $`git -C ${clone} checkout -B ${branch} ${sha} --quiet`; - - // The clone only carries refs/heads/$BRANCH locally; seed the other - // configured branches from origin's tracking refs so semantic-release's - // branch validator sees them. A maintenance branch needs main present to compute its range. - for (const cfg of ["main", "develop", "next"]) { - if (cfg === branch) continue; - const refSha = await $`git -C ${clone} rev-parse --verify -q refs/remotes/origin/${cfg}` - .nothrow() - .quiet(); - if (refSha.exitCode === 0) { - await $`git -C ${clone} update-ref refs/heads/${cfg} ${refSha.text().trim()}`; + for (const plugin of plugins) { + const specifier = releasePluginSpecifier(plugin); + if (!specifier || path.isAbsolute(specifier) || !specifier.startsWith(".")) continue; + + const sourcePath = path.resolve(cliDir, specifier); + const relativePath = path.relative(cliDir, sourcePath); + if ( + relativePath === ".." || + relativePath.startsWith(`..${path.sep}`) || + path.isAbsolute(relativePath) + ) { + return yield* new LocalReleasePluginOutsideCli({ + message: `Local release plugin must stay inside apps/cli: ${specifier}`, + }); + } + + const destinationPath = path.join(historicalCliDir, relativePath); + yield* fs.makeDirectory(path.dirname(destinationPath), { recursive: true }); + yield* fs.copyFile(sourcePath, destinationPath); } - } + }); - // semantic-release's notes reader returns channels=[null] for any tag - // missing an annotation, and the prerelease filter drops those tags - // entirely — walking lastRelease back far enough to drag unrelated commits - // into the changelog. Seed a channel note for every reachable tag that - // lacks one, inferring the channel from the tag name. - const mergedTagsOut = await $`git -C ${clone} tag --merged HEAD --sort=v:refname`.text(); - const mergedTags = mergedTagsOut.split("\n").filter((t) => t && t !== tag); - for (const prevTag of mergedTags) { - const noteCheck = await $`git -C ${clone} notes --ref semantic-release show ${prevTag}` - .nothrow() - .quiet(); - if (noteCheck.exitCode === 0) continue; - const payload = JSON.stringify({ channels: [channelForTag(prevTag)] }); - await $`git -C ${clone} notes --ref semantic-release add -f -m ${payload} ${prevTag}^{commit}`.quiet(); + const repoField = rootPkg.repository?.url ?? rootPkg.repository ?? ""; + const repoUrl = `${String(repoField) + .replace(/^git\+/, "") + .replace(/\.git$/, "") + .replace(/\/$/, "")}.git`; + if (!repoUrl.startsWith("http")) { + yield* Console.error( + `Could not derive repository URL from apps/cli/package.json (got: ${repoUrl})`, + ); + return yield* Effect.sync(() => process.exit(1)); } - // Apply the current release config to the historical checkout, so the notes - // come out in the right shape regardless of what release config shipped at the tag. - const clonePkgPath = path.join(clone, "apps/cli/package.json"); - const clonePkg = JSON.parse(await readFile(clonePkgPath, "utf8")); - clonePkg.release = rootPkg.release; - await writeFile(clonePkgPath, `${JSON.stringify(clonePkg, null, 2)}\n`); - await copyLocalReleasePlugins(clone); - - // semantic-release exits with "behind remote" whenever the remote tip - // differs from HEAD, which it always does when backfilling an old tag. Use - // git's insteadOf to redirect the GitHub URL to the local clone for this - // run, while semantic-release still renders commit/PR links against the real URL. - await $`git -C ${clone} config --local url.file://${clone}.insteadOf ${repoUrl}`; - - console.error(`==> Re-staged on ${branch} @ ${sha} (without tag ${tag})`); - console.error(`==> Running semantic-release --dry-run`); - - // env-ci detects the branch from GITHUB_REF (and friends) even with `noCi: - // true`, which only bypasses the "not in CI" guard. That can name a branch - // other than the one the clone's HEAD actually points at, so strip the - // GitHub Actions detection vars and let env-ci read HEAD directly. - const childEnv = { ...process.env }; - for (const key of [ - "GITHUB_ACTIONS", - "GITHUB_REF", - "GITHUB_REF_NAME", - "GITHUB_HEAD_REF", - "GITHUB_BASE_REF", - "GITHUB_EVENT_NAME", - "CI", - ]) { - delete childEnv[key]; + const tagCheck = yield* shell(() => + $`git rev-parse -q --verify refs/tags/${tag}`.cwd(repoRoot).nothrow().quiet(), + ); + if (tagCheck.exitCode !== 0) { + yield* Console.error(`Tag ${tag} not found locally. Try: git fetch --tags origin`); + return yield* Effect.sync(() => process.exit(1)); } - const result = await semanticRelease( - { dryRun: true, noCi: true, repositoryUrl: repoUrl }, - { - cwd: path.join(clone, "apps/cli"), - env: childEnv, - stdout: process.stderr, - stderr: process.stderr, - }, - ); + const backfill = (work: string) => + Effect.gen(function* () { + const clone = path.join(work, "repo"); - if (!result || !result.nextRelease) { - console.error(`semantic-release did not compute a next release for ${tag}`); - process.exit(1); - } + yield* Console.error(`==> Cloning ${repoRoot} -> ${clone}`); + yield* shell(() => $`git clone --quiet --no-local ${repoRoot} ${clone}`); - const expected = tag.replace(/^v/, ""); - if (result.nextRelease.version !== expected) { - console.error( - `semantic-release computed v${result.nextRelease.version} but expected ${tag}; ` + - `check channel notes and release config`, - ); - process.exit(1); - } + // `git notes add` (used below to seed channel notes) requires a committer + // identity, which CI runners don't ship in ~/.gitconfig. + yield* shell( + () => $`git -C ${clone} config --local user.email backfill-release-notes@supabase.local`, + ); + yield* shell(() => $`git -C ${clone} config --local user.name backfill-release-notes`); + // A signing key inherited from a user's global config would also make + // `git notes add` fail; the temp clone never publishes anything, so disable it. + yield* shell(() => $`git -C ${clone} config --local commit.gpgsign false`); + yield* shell(() => $`git -C ${clone} config --local tag.gpgsign false`); - const notes = result.nextRelease.notes ?? ""; + const originUrl = (yield* shell(() => + $`git -C ${repoRoot} remote get-url origin`.text(), + )).trim(); - if (apply) { - const notesFile = path.join(work, "notes.md"); - await writeFile(notesFile, notes); - console.error(`==> Updating GitHub Release body for ${tag}`); - await $`gh release edit ${tag} --notes-file ${notesFile}`; - } else { - process.stdout.write(notes); - if (!notes.endsWith("\n")) process.stdout.write("\n"); - } -} finally { - await rm(work, { recursive: true, force: true }); + // Notes refs aren't fetched by `git clone`. Pull from the source repo first + // (network-free), then from origin so even tags whose notes haven't been + // sync'd locally yet are available. + yield* shell(() => + $`git -C ${clone} fetch --no-tags --quiet ${repoRoot} +refs/notes/*:refs/notes/*` + .nothrow() + .quiet(), + ); + yield* shell(() => + $`git -C ${clone} fetch --no-tags --quiet ${originUrl} +refs/notes/*:refs/notes/*` + .nothrow() + .quiet(), + ); + // One fetch per branch: git aborts a multi-ref fetch when any named ref is missing, and `next` + // may not exist. The `v*.x` glob matches zero or more branches without failing. + for (const branchSpec of ["main", "develop", "next", "v*.x"]) { + yield* shell(() => + $`git -C ${clone} fetch --no-tags --quiet ${originUrl} +refs/heads/${branchSpec}:refs/remotes/origin/${branchSpec}` + .nothrow() + .quiet(), + ); + } + + const sha = (yield* shell(() => $`git -C ${clone} rev-list -n 1 ${tag}`.text())).trim(); + const onMain = + (yield* shell(() => + $`git -C ${clone} merge-base --is-ancestor ${sha} refs/remotes/origin/main` + .nothrow() + .quiet(), + )).exitCode === 0; + const branch = releaseBranchForTag(tag, onMain); + if ( + branch !== "main" && + branch !== "develop" && + branch !== "next" && + (yield* shell(() => + $`git -C ${clone} rev-parse --verify -q refs/remotes/origin/${branch}`.nothrow().quiet(), + )).exitCode !== 0 + ) { + yield* Console.error(`${tag} belongs to ${branch}, but origin/${branch} does not exist.`); + return yield* Effect.sync(() => process.exit(1)); + } + // Delete the target tag and any other local tag pointing at the same commit: + // when a stable and a beta share a commit, semantic-release picks the + // higher-semver one as lastRelease (which becomes HEAD itself, leaving "no + // release"). Dropping the co-incident tags falls back to the genuine prior release. + const coincidentTagsOut = yield* shell(() => + $`git -C ${clone} tag --points-at ${sha}`.text(), + ); + const coincidentTags = coincidentTagsOut + .split("\n") + .map((t) => t.trim()) + .filter(Boolean); + for (const ct of coincidentTags) { + yield* shell(() => $`git -C ${clone} tag -d ${ct}`.quiet().nothrow()); + } + yield* shell(() => $`git -C ${clone} checkout -B ${branch} ${sha} --quiet`); + + // The clone only carries refs/heads/$BRANCH locally; seed the other + // configured branches from origin's tracking refs so semantic-release's + // branch validator sees them. A maintenance branch needs main present to compute its range. + for (const cfg of ["main", "develop", "next"]) { + if (cfg === branch) continue; + const refSha = yield* shell(() => + $`git -C ${clone} rev-parse --verify -q refs/remotes/origin/${cfg}`.nothrow().quiet(), + ); + if (refSha.exitCode === 0) { + yield* shell( + () => $`git -C ${clone} update-ref refs/heads/${cfg} ${refSha.text().trim()}`, + ); + } + } + + // semantic-release's notes reader returns channels=[null] for any tag + // missing an annotation, and the prerelease filter drops those tags + // entirely — walking lastRelease back far enough to drag unrelated commits + // into the changelog. Seed a channel note for every reachable tag that + // lacks one, inferring the channel from the tag name. + const mergedTagsOut = yield* shell(() => + $`git -C ${clone} tag --merged HEAD --sort=v:refname`.text(), + ); + const mergedTags = mergedTagsOut.split("\n").filter((t) => t && t !== tag); + for (const prevTag of mergedTags) { + const noteCheck = yield* shell(() => + $`git -C ${clone} notes --ref semantic-release show ${prevTag}`.nothrow().quiet(), + ); + if (noteCheck.exitCode === 0) continue; + const payload = yield* encodeJson({ channels: [channelForTag(prevTag)] }); + yield* shell(() => + $`git -C ${clone} notes --ref semantic-release add -f -m ${payload} ${prevTag}^{commit}`.quiet(), + ); + } + + // Apply the current release config to the historical checkout, so the notes + // come out in the right shape regardless of what release config shipped at the tag. + const clonePkgPath = path.join(clone, "apps/cli/package.json"); + const clonePkg = yield* decodeJson(utf8.decode(yield* fs.readFile(clonePkgPath))); + clonePkg.release = rootPkg.release; + yield* fs.writeFileString(clonePkgPath, `${yield* encodePackageJson(clonePkg)}\n`); + yield* copyLocalReleasePlugins(clone); + + // semantic-release exits with "behind remote" whenever the remote tip + // differs from HEAD, which it always does when backfilling an old tag. Use + // git's insteadOf to redirect the GitHub URL to the local clone for this + // run, while semantic-release still renders commit/PR links against the real URL. + yield* shell( + () => $`git -C ${clone} config --local url.file://${clone}.insteadOf ${repoUrl}`, + ); + + yield* Console.error(`==> Re-staged on ${branch} @ ${sha} (without tag ${tag})`); + yield* Console.error(`==> Running semantic-release --dry-run`); + + // env-ci detects the branch from GITHUB_REF (and friends) even with `noCi: + // true`, which only bypasses the "not in CI" guard. That can name a branch + // other than the one the clone's HEAD actually points at, so strip the + // GitHub Actions detection vars and let env-ci read HEAD directly. + const childEnv = { ...process.env }; + for (const key of [ + "GITHUB_ACTIONS", + "GITHUB_REF", + "GITHUB_REF_NAME", + "GITHUB_HEAD_REF", + "GITHUB_BASE_REF", + "GITHUB_EVENT_NAME", + "CI", + ]) { + delete childEnv[key]; + } + + const result = yield* Effect.tryPromise({ + try: () => + semanticRelease( + { dryRun: true, noCi: true, repositoryUrl: repoUrl }, + { + cwd: path.join(clone, "apps/cli"), + env: childEnv, + stdout: process.stderr, + stderr: process.stderr, + }, + ), + catch: (cause) => new SemanticReleaseFailed({ cause }), + }); + + if (!result || !result.nextRelease) { + yield* Console.error(`semantic-release did not compute a next release for ${tag}`); + return yield* Effect.sync(() => process.exit(1)); + } + + const expected = tag.replace(/^v/, ""); + if (result.nextRelease.version !== expected) { + yield* Console.error( + `semantic-release computed v${result.nextRelease.version} but expected ${tag}; ` + + `check channel notes and release config`, + ); + return yield* Effect.sync(() => process.exit(1)); + } + + const notes = result.nextRelease.notes ?? ""; + + if (apply) { + const notesFile = path.join(work, "notes.md"); + yield* fs.writeFileString(notesFile, notes); + yield* Console.error(`==> Updating GitHub Release body for ${tag}`); + yield* shell(() => $`gh release edit ${tag} --notes-file ${notesFile}`); + } else { + yield* Effect.sync(() => { + process.stdout.write(notes); + if (!notes.endsWith("\n")) process.stdout.write("\n"); + }); + } + }); + /* A failed removal replaces the run's outcome. */ + yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const work = yield* fs.makeTempDirectory({ prefix: "backfill-release-notes." }); + const outcome = yield* Effect.exit(restore(backfill(work))); + yield* fs.remove(work, { recursive: true, force: true }); + return yield* outcome; + }), + ); +}); + +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + return error.cause !== undefined ? error.cause : new Error(error.message); } + +await Effect.runPromise( + main.pipe( + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + Effect.catch((error) => Effect.die(thrownValue(error))), + ), +); diff --git a/apps/cli/scripts/bench-cli.ts b/apps/cli/scripts/bench-cli.ts index 7e7530bb63..6e1df06c16 100644 --- a/apps/cli/scripts/bench-cli.ts +++ b/apps/cli/scripts/bench-cli.ts @@ -6,11 +6,10 @@ */ import { randomUUID } from "node:crypto"; import { tmpdir } from "node:os"; -import { resolve as resolvePath } from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; -import { BunRuntime, BunServices } from "@effect/platform-bun"; -import { Clock, Console, Effect, FileSystem, Path, Ref, Stdio } from "effect"; +import { BunPath, BunRuntime, BunServices } from "@effect/platform-bun"; +import { Clock, Console, Effect, FileSystem, Path, Ref, Schema, Stdio } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; import { readSpans, type ReportSpan } from "./trace-report.ts"; @@ -35,6 +34,8 @@ export interface BenchOptions { readonly updateCheck: boolean; } +const ReportJson = Schema.fromJsonString(Schema.Unknown, { space: 2 }); + const DEFAULT_COMMANDS: ReadonlyArray> = [["--version"], ["--help"]]; /** Whether `--help`/`-h` is passed as a flag rather than as a `--command` value. */ @@ -90,11 +91,15 @@ export function parseBenchArgs(argv: ReadonlyArray): BenchOptions { throw new Error('--cwd-setup only supports "init"'); } + /* The host `Path` service (`node:path` itself), read synchronously for this sync parser. */ + const hostPath = Effect.runSync( + Path.Path.useSync((path) => path).pipe(Effect.provide(BunPath.layer)), + ); return { // Each launch spawns with a per-run `cwd`, which also resolves a relative command path; a // path given relative to the invocation directory must become absolute before that happens. - base: resolvePath(values.base), - branch: resolvePath(values.branch), + base: hostPath.resolve(values.base), + branch: hostPath.resolve(values.branch), runs, warmup, commands: @@ -590,10 +595,15 @@ const main = Effect.gen(function* () { if (options.out !== undefined) { const fs = yield* FileSystem.FileSystem; - yield* fs.writeFileString(options.out, JSON.stringify({ report, samples }, null, 2)); + yield* fs.writeFileString( + options.out, + yield* Schema.encodeEffect(ReportJson)({ report, samples }), + ); } - yield* Console.log(options.json ? JSON.stringify(report, null, 2) : formatReport(report)); + yield* Console.log( + options.json ? yield* Schema.encodeEffect(ReportJson)(report) : formatReport(report), + ); }); if (import.meta.main) { diff --git a/apps/cli/scripts/build-binary.integration.test.ts b/apps/cli/scripts/build-binary.integration.test.ts index 8073bee193..f3ee9dad24 100644 --- a/apps/cli/scripts/build-binary.integration.test.ts +++ b/apps/cli/scripts/build-binary.integration.test.ts @@ -1,7 +1,6 @@ -import { afterEach, describe, expect, test } from "vitest"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, FileSystem, Layer, Path, Schema } from "effect"; import { fileURLToPath } from "node:url"; const fixturePath = fileURLToPath( @@ -10,89 +9,111 @@ const fixturePath = fileURLToPath( const versionFixturePath = fileURLToPath( new URL("../tests/fixtures/compiled-cli-version.ts", import.meta.url), ); -const temporaryDirectories: string[] = []; +const platformLayer = Layer.mergeAll(BunFileSystem.layer, BunPath.layer); -afterEach(async () => { - await Promise.all( - temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })), - ); -}); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.String)); describe("compiled binary assets", () => { - test("embeds and loads libpg-query.wasm", async () => { - const directory = await mkdtemp(path.join(tmpdir(), "supabase-compiled-wasm-")); - temporaryDirectories.push(directory); - const executable = path.join(directory, "parser-probe"); - const bunExecutable = Bun.which("bun"); - if (!bunExecutable) { - throw new Error("Bun executable not found"); - } + it.live( + "embeds and loads libpg-query.wasm", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const directory = yield* fs.makeTempDirectoryScoped({ prefix: "supabase-compiled-wasm-" }); + const executable = path.join(directory, "parser-probe"); + const bunExecutable = Bun.which("bun"); + if (!bunExecutable) { + throw new Error("Bun executable not found"); + } - const build = Bun.spawn( - [bunExecutable, "build", fixturePath, "--compile", `--outfile=${executable}`], - { stdout: "pipe", stderr: "pipe" }, - ); - const [buildExitCode, buildStderr] = await Promise.all([ - build.exited, - new Response(build.stderr).text(), - ]); - expect(buildExitCode, buildStderr).toBe(0); + const build = Bun.spawn( + [bunExecutable, "build", fixturePath, "--compile", `--outfile=${executable}`], + { stdout: "pipe", stderr: "pipe" }, + ); + const [buildExitCode, buildStderr] = yield* Effect.all( + [ + Effect.promise(() => build.exited), + Effect.promise(() => new Response(build.stderr).text()), + ], + { concurrency: "unbounded" }, + ); + expect(buildExitCode, buildStderr).toBe(0); - const probe = Bun.spawn([executable], { - cwd: directory, - env: {}, - stdout: "pipe", - stderr: "pipe", - }); - const [probeExitCode, stdout, stderr] = await Promise.all([ - probe.exited, - new Response(probe.stdout).text(), - new Response(probe.stderr).text(), - ]); + const probe = Bun.spawn([executable], { + cwd: directory, + env: {}, + stdout: "pipe", + stderr: "pipe", + }); + const [probeExitCode, stdout, stderr] = yield* Effect.all( + [ + Effect.promise(() => probe.exited), + Effect.promise(() => new Response(probe.stdout).text()), + Effect.promise(() => new Response(probe.stderr).text()), + ], + { concurrency: "unbounded" }, + ); - expect(probeExitCode, stderr).toBe(0); - expect(stdout).toContain("libpg-query.wasm loaded"); - }, 20_000); + expect(probeExitCode, stderr).toBe(0); + expect(stdout).toContain("libpg-query.wasm loaded"); + }).pipe(Effect.provide(platformLayer)), + 20_000, + ); - test("embeds the build version independently of the runtime environment", async () => { - const directory = await mkdtemp(path.join(tmpdir(), "supabase-compiled-version-")); - temporaryDirectories.push(directory); - const executable = path.join(directory, "version-probe"); - const bunExecutable = Bun.which("bun"); - if (!bunExecutable) { - throw new Error("Bun executable not found"); - } + it.live( + "embeds the build version independently of the runtime environment", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const directory = yield* fs.makeTempDirectoryScoped({ + prefix: "supabase-compiled-version-", + }); + const executable = path.join(directory, "version-probe"); + const bunExecutable = Bun.which("bun"); + if (!bunExecutable) { + throw new Error("Bun executable not found"); + } - const build = Bun.spawn( - [ - bunExecutable, - "build", - versionFixturePath, - "--compile", - `--define=SUPABASE_CLI_VERSION=${JSON.stringify("7.8.9-beta.1")}`, - `--outfile=${executable}`, - ], - { stdout: "pipe", stderr: "pipe" }, - ); - const [buildExitCode, buildStderr] = await Promise.all([ - build.exited, - new Response(build.stderr).text(), - ]); - expect(buildExitCode, buildStderr).toBe(0); + const build = Bun.spawn( + [ + bunExecutable, + "build", + versionFixturePath, + "--compile", + `--define=SUPABASE_CLI_VERSION=${yield* encodeJson("7.8.9-beta.1")}`, + `--outfile=${executable}`, + ], + { stdout: "pipe", stderr: "pipe" }, + ); + const [buildExitCode, buildStderr] = yield* Effect.all( + [ + Effect.promise(() => build.exited), + Effect.promise(() => new Response(build.stderr).text()), + ], + { concurrency: "unbounded" }, + ); + expect(buildExitCode, buildStderr).toBe(0); - const probe = Bun.spawn([executable], { - cwd: directory, - env: { SUPABASE_CLI_VERSION: "9.9.9" }, - stdout: "pipe", - stderr: "pipe", - }); - const [probeExitCode, stdout, stderr] = await Promise.all([ - probe.exited, - new Response(probe.stdout).text(), - new Response(probe.stderr).text(), - ]); + const probe = Bun.spawn([executable], { + cwd: directory, + env: { SUPABASE_CLI_VERSION: "9.9.9" }, + stdout: "pipe", + stderr: "pipe", + }); + const [probeExitCode, stdout, stderr] = yield* Effect.all( + [ + Effect.promise(() => probe.exited), + Effect.promise(() => new Response(probe.stdout).text()), + Effect.promise(() => new Response(probe.stderr).text()), + ], + { concurrency: "unbounded" }, + ); - expect(probeExitCode, stderr).toBe(0); - expect(stdout.trim()).toBe("7.8.9-beta.1"); - }, 20_000); + expect(probeExitCode, stderr).toBe(0); + expect(stdout.trim()).toBe("7.8.9-beta.1"); + }).pipe(Effect.provide(platformLayer)), + 20_000, + ); }); diff --git a/apps/cli/scripts/build-binary.ts b/apps/cli/scripts/build-binary.ts index 28ed04fee9..a92ca1d242 100644 --- a/apps/cli/scripts/build-binary.ts +++ b/apps/cli/scripts/build-binary.ts @@ -1,4 +1,4 @@ -import { Effect } from "effect"; +import { Console, Effect } from "effect"; import { bundleServeMainTemplate } from "../src/shared/functions/serve-main-bundler.ts"; import { compileOptions, stackReleaseDefine } from "./compile-options.ts"; @@ -32,5 +32,5 @@ const result = await Bun.build({ }, }); for (const log of result.logs) { - console.warn(log); + Effect.runSync(Console.warn(log)); } diff --git a/apps/cli/scripts/build.ts b/apps/cli/scripts/build.ts index 3d7f9db378..c911034494 100644 --- a/apps/cli/scripts/build.ts +++ b/apps/cli/scripts/build.ts @@ -1,10 +1,9 @@ import { $ } from "bun"; import { createHash } from "node:crypto"; -import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises"; -import path from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; -import { Effect } from "effect"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { Config, Console, Data, Effect, FileSystem, Layer, Path, Schema } from "effect"; import { bundleServeMainTemplate } from "../src/shared/functions/serve-main-bundler.ts"; import { compileOptions, stackReleaseDefine } from "./compile-options.ts"; import { darwinBinaries, MACOS_IDENTIFIERS } from "./macos-signing.ts"; @@ -30,373 +29,431 @@ const { values } = parseArgs({ }, }); -const root = path.resolve(import.meta.dir, "../../.."); -const packageJsonPath = path.join(root, "apps/cli/package.json"); -const packageVersion = JSON.parse(await readFile(packageJsonPath, "utf8")) as { version?: string }; -const version = values.version ?? packageVersion.version; -if (!version) { - console.error("Usage: bun apps/cli/scripts/build.ts [--version ]"); - process.exit(1); -} -if (values.version === undefined) { - console.warn( - `[build] --version not provided; falling back to package.json version "${version}". Pass --version explicitly in release builds.`, - ); -} - -const TARGETS = [ - { - bunTarget: "bun-darwin-arm64", - pkg: "cli-darwin-arm64", - archive: `supabase_${version}_darwin_arm64.tar.gz`, - ext: "", - }, - { - bunTarget: "bun-darwin-x64", - pkg: "cli-darwin-x64", - archive: `supabase_${version}_darwin_amd64.tar.gz`, - ext: "", - }, - { - bunTarget: "bun-linux-arm64", - pkg: "cli-linux-arm64", - archive: `supabase_${version}_linux_arm64.tar.gz`, - nfpmArch: "arm64", - ext: "", - }, - { - bunTarget: "bun-linux-x64-baseline", - pkg: "cli-linux-x64", - archive: `supabase_${version}_linux_amd64.tar.gz`, - nfpmArch: "amd64", - ext: "", - }, - { - bunTarget: "bun-windows-x64-baseline", - pkg: "cli-windows-x64", - archive: `supabase_${version}_windows_amd64.zip`, - ext: ".exe", - }, - { - bunTarget: "bun-windows-arm64", - pkg: "cli-windows-arm64", - archive: `supabase_${version}_windows_arm64.zip`, - ext: ".exe", - }, -] as const; - -const entrypoint = path.join(root, "apps/cli/src/main.ts"); -const distDir = path.join(root, "dist"); -const goSource = path.resolve(root, "apps/cli-go"); -const buildDefines = { - ...(await stackReleaseDefine()), - SUPABASE_FUNCTIONS_SERVE_MAIN_TEMPLATE: JSON.stringify( - await Effect.runPromise(bundleServeMainTemplate()), - ), - SUPABASE_CLI_POSTHOG_KEY: JSON.stringify(process.env.POSTHOG_API_KEY ?? ""), - SUPABASE_CLI_POSTHOG_HOST: JSON.stringify(process.env.POSTHOG_ENDPOINT ?? ""), - // Skips msgpackr's startup probe for its native addon at the build host's store path, which - // on macOS goes through the automounter and can hang every command (supabase/cli#6771). - "process.env.MSGPACKR_NATIVE_ACCELERATION_DISABLED": JSON.stringify("true"), -}; - -type BunTarget = (typeof TARGETS)[number]["bunTarget"]; - -const GO_TARGETS: Record = { - "bun-darwin-arm64": { goos: "darwin", goarch: "arm64" }, - "bun-darwin-x64": { goos: "darwin", goarch: "amd64" }, - "bun-linux-arm64": { goos: "linux", goarch: "arm64" }, - "bun-linux-x64-baseline": { goos: "linux", goarch: "amd64" }, - "bun-windows-x64-baseline": { goos: "windows", goarch: "amd64" }, - "bun-windows-arm64": { goos: "windows", goarch: "arm64" }, -}; - -type SignMode = "adhoc" | "off"; - -async function runBunBuild(config: Bun.BuildConfig) { - const result = await Bun.build({ - ...config, - ...compileOptions, - plugins: config.plugins ?? [], - }); - for (const log of result.logs) { - console.warn(log); - } -} +class BuildStepFailed extends Data.TaggedError("BuildStepFailed")<{ readonly cause: unknown }> {} -async function buildTarget(target: (typeof TARGETS)[number]) { - const binDir = path.join(root, "packages", target.pkg, "bin"); - await mkdir(binDir, { recursive: true }); +const step = (run: () => PromiseLike) => + Effect.tryPromise({ try: run, catch: (cause) => new BuildStepFailed({ cause }) }); - const outfile = path.join(binDir, `supabase${target.ext}`); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); - console.log(`[${target.pkg}] Compiling Bun CLI...`); - await runBunBuild({ - entrypoints: [entrypoint], - compile: { target: target.bunTarget, outfile }, - define: { - ...buildDefines, - SUPABASE_CLI_VERSION: JSON.stringify(version), - }, - }); - console.log(`[${target.pkg}] Done.`); -} +/* Native `JSON.parse`, so a malformed package.json fails with its own `SyntaxError`. */ +const parsePackageJson = (text: string) => JSON.parse(text) as { version?: string }; -async function buildGoTarget(target: (typeof TARGETS)[number]) { - const binDir = path.join(root, "packages", target.pkg, "bin"); - await mkdir(binDir, { recursive: true }); +const envOrEmpty = (name: string) => Config.String(name).pipe(Config.withDefault("")); - const { goos, goarch } = GO_TARGETS[target.bunTarget]; - const outfile = path.join(binDir, `supabase-go${target.ext}`); +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + /* Concurrent jobs run as promises joined by `Promise.all`, so one failed job does not + interrupt the others. */ + const runPromise = Effect.runPromiseWith(yield* Effect.context()); + const runJob = ( + job: Effect.Effect, + ) => runPromise(job.pipe(Effect.catch((error) => Effect.die(thrownValue(error))))); - console.log(`[${target.pkg}] Compiling Go CLI (${goos}/${goarch})...`); - const ldflagParts = ["-s", "-w", `-X github.com/supabase/cli/internal/utils.Version=${version}`]; - const { SENTRY_DSN, POSTHOG_API_KEY, POSTHOG_ENDPOINT } = process.env; - if (SENTRY_DSN) { - ldflagParts.push(`-X github.com/supabase/cli/internal/utils.SentryDsn=${SENTRY_DSN}`); - } - if (POSTHOG_API_KEY) { - ldflagParts.push(`-X github.com/supabase/cli/internal/utils.PostHogAPIKey=${POSTHOG_API_KEY}`); - } - if (POSTHOG_ENDPOINT) { - ldflagParts.push( - `-X github.com/supabase/cli/internal/utils.PostHogEndpoint=${POSTHOG_ENDPOINT}`, - ); - } - const goLdflags = ldflagParts.join(" "); - await $`go build -trimpath -ldflags=${goLdflags} -o ${outfile} .`.cwd(goSource).env({ - ...process.env, - GOOS: goos, - GOARCH: goarch, - CGO_ENABLED: "0", + const root = path.resolve(import.meta.dir, "../../.."); + const packageJsonPath = path.join(root, "apps/cli/package.json"); + const packageJsonText = new TextDecoder("utf-8", { ignoreBOM: true }).decode( + yield* fs.readFile(packageJsonPath), + ); + const packageVersion = yield* Effect.try({ + try: () => parsePackageJson(packageJsonText), + catch: (cause) => new BuildStepFailed({ cause }), }); - console.log(`[${target.pkg}] Go binary done.`); -} - -/** - * Decides how to sign macOS binaries. `rcodesign` signs Mach-O binaries from Linux, so signing - * runs inline on this build runner; falls back to "off" with a warning unless - * SUPABASE_CLI_REQUIRE_SIGNING=1, which fails the build when the tool is missing. - */ -function resolveSignMode(): SignMode { - if (Bun.which("rcodesign")) { - return "adhoc"; + const version = values.version ?? packageVersion.version; + if (!version) { + yield* Console.error("Usage: bun apps/cli/scripts/build.ts [--version ]"); + process.exit(1); } - if (process.env.SUPABASE_CLI_REQUIRE_SIGNING === "1") { - console.error( - "rcodesign not found but SUPABASE_CLI_REQUIRE_SIGNING=1 is set. Release builds " + - "must sign the macOS binaries. Install rcodesign (https://github.com/indygreg/apple-platform-rs).", + if (values.version === undefined) { + yield* Console.warn( + `[build] --version not provided; falling back to package.json version "${version}". Pass --version explicitly in release builds.`, ); - process.exit(1); } - console.warn( - "WARNING: rcodesign not found — macOS binaries will keep Bun's linker-signed ad-hoc " + - "signature, which macOS 26+ SIGKILLs at launch (CLI-1621). Install rcodesign to " + - "produce runnable macOS binaries locally.", - ); - return "off"; -} -async function signDarwinBinaries(mode: SignMode) { - if (mode === "off") { - return; - } + const TARGETS = [ + { + bunTarget: "bun-darwin-arm64", + pkg: "cli-darwin-arm64", + archive: `supabase_${version}_darwin_arm64.tar.gz`, + ext: "", + }, + { + bunTarget: "bun-darwin-x64", + pkg: "cli-darwin-x64", + archive: `supabase_${version}_darwin_amd64.tar.gz`, + ext: "", + }, + { + bunTarget: "bun-linux-arm64", + pkg: "cli-linux-arm64", + archive: `supabase_${version}_linux_arm64.tar.gz`, + nfpmArch: "arm64", + ext: "", + }, + { + bunTarget: "bun-linux-x64-baseline", + pkg: "cli-linux-x64", + archive: `supabase_${version}_linux_amd64.tar.gz`, + nfpmArch: "amd64", + ext: "", + }, + { + bunTarget: "bun-windows-x64-baseline", + pkg: "cli-windows-x64", + archive: `supabase_${version}_windows_amd64.zip`, + ext: ".exe", + }, + { + bunTarget: "bun-windows-arm64", + pkg: "cli-windows-arm64", + archive: `supabase_${version}_windows_arm64.zip`, + ext: ".exe", + }, + ] as const; + + const entrypoint = path.join(root, "apps/cli/src/main.ts"); + const distDir = path.join(root, "dist"); + const goSource = path.resolve(root, "apps/cli-go"); + const buildDefines = { + ...(yield* step(() => stackReleaseDefine())), + SUPABASE_FUNCTIONS_SERVE_MAIN_TEMPLATE: yield* encodeJson(yield* bundleServeMainTemplate()), + SUPABASE_CLI_POSTHOG_KEY: yield* encodeJson(yield* envOrEmpty("POSTHOG_API_KEY")), + SUPABASE_CLI_POSTHOG_HOST: yield* encodeJson(yield* envOrEmpty("POSTHOG_ENDPOINT")), + // Skips msgpackr's startup probe for its native addon at the build host's store path, which + // on macOS goes through the automounter and can hang every command (supabase/cli#6771). + "process.env.MSGPACKR_NATIVE_ACCELERATION_DISABLED": yield* encodeJson("true"), + }; + + type BunTarget = (typeof TARGETS)[number]["bunTarget"]; + + const GO_TARGETS: Record = { + "bun-darwin-arm64": { goos: "darwin", goarch: "arm64" }, + "bun-darwin-x64": { goos: "darwin", goarch: "amd64" }, + "bun-linux-arm64": { goos: "linux", goarch: "arm64" }, + "bun-linux-x64-baseline": { goos: "linux", goarch: "amd64" }, + "bun-windows-x64-baseline": { goos: "windows", goarch: "amd64" }, + "bun-windows-arm64": { goos: "windows", goarch: "arm64" }, + }; + + type SignMode = "adhoc" | "off"; + + const runBunBuild = Effect.fnUntraced(function* (config: Bun.BuildConfig) { + const result = yield* step(() => + Bun.build({ + ...config, + ...compileOptions, + plugins: config.plugins ?? [], + }), + ); + for (const log of result.logs) { + yield* Console.warn(log); + } + }); - const darwinTargets = TARGETS.filter((target) => target.bunTarget.startsWith("bun-darwin")); - const binaries = darwinBinaries(); + const buildTarget = Effect.fnUntraced(function* (target: (typeof TARGETS)[number]) { + const binDir = path.join(root, "packages", target.pkg, "bin"); + yield* fs.makeDirectory(binDir, { recursive: true }); + + const outfile = path.join(binDir, `supabase${target.ext}`); + + yield* Console.log(`[${target.pkg}] Compiling Bun CLI...`); + yield* runBunBuild({ + entrypoints: [entrypoint], + compile: { target: target.bunTarget, outfile }, + define: { + ...buildDefines, + SUPABASE_CLI_VERSION: yield* encodeJson(version), + }, + }); + yield* Console.log(`[${target.pkg}] Done.`); + }); - for (const target of darwinTargets) { + const buildGoTarget = Effect.fnUntraced(function* (target: (typeof TARGETS)[number]) { const binDir = path.join(root, "packages", target.pkg, "bin"); + yield* fs.makeDirectory(binDir, { recursive: true }); - for (const binary of binaries) { - const binPath = path.join(binDir, binary); - const identifier = MACOS_IDENTIFIERS[binary]; - - console.log(`[${target.pkg}] Ad-hoc signing ${binary} (${identifier})...`); - // No key material, so rcodesign produces an ad-hoc signature, equivalent to - // `codesign --sign -`, replacing Bun/Go's linker-signed one. - await $`rcodesign sign --binary-identifier ${identifier} ${binPath}`; - - // Matches the identifier's whole value, so the SFE's `com.supabase.cli` can't satisfy - // the sidecar's `com.supabase.cli-go`, and confirms the signature is no longer linker-signed. - const info = await $`rcodesign print-signature-info ${binPath}`.text(); - const signedIdentifier = info.match(/^\s*identifier:\s*(\S+)\s*$/m)?.[1]; - if (signedIdentifier !== identifier) { - throw new Error( - `Signing ${binPath} failed: expected identifier ${identifier}, got ${signedIdentifier ?? "none"}`, - ); - } - if (info.includes("LINKER_SIGNED")) { - throw new Error(`Signing ${binPath} failed: signature is still linker-signed`); - } + const { goos, goarch } = GO_TARGETS[target.bunTarget]; + const outfile = path.join(binDir, `supabase-go${target.ext}`); + + yield* Console.log(`[${target.pkg}] Compiling Go CLI (${goos}/${goarch})...`); + const ldflagParts = [ + "-s", + "-w", + `-X github.com/supabase/cli/internal/utils.Version=${version}`, + ]; + const { SENTRY_DSN, POSTHOG_API_KEY, POSTHOG_ENDPOINT } = process.env; + if (SENTRY_DSN) { + ldflagParts.push(`-X github.com/supabase/cli/internal/utils.SentryDsn=${SENTRY_DSN}`); } - } -} + if (POSTHOG_API_KEY) { + ldflagParts.push( + `-X github.com/supabase/cli/internal/utils.PostHogAPIKey=${POSTHOG_API_KEY}`, + ); + } + if (POSTHOG_ENDPOINT) { + ldflagParts.push( + `-X github.com/supabase/cli/internal/utils.PostHogEndpoint=${POSTHOG_ENDPOINT}`, + ); + } + const goLdflags = ldflagParts.join(" "); + yield* step(() => + $`go build -trimpath -ldflags=${goLdflags} -o ${outfile} .`.cwd(goSource).env({ + ...process.env, + GOOS: goos, + GOARCH: goarch, + CGO_ENABLED: "0", + }), + ); + yield* Console.log(`[${target.pkg}] Go binary done.`); + }); -async function archiveTarget(target: (typeof TARGETS)[number]) { - const binDir = path.join(root, "packages", target.pkg, "bin"); - const archivePath = path.join(distDir, target.archive); + /** + * Decides how to sign macOS binaries. `rcodesign` signs Mach-O binaries from Linux, so signing + * runs inline on this build runner; falls back to "off" with a warning unless + * SUPABASE_CLI_REQUIRE_SIGNING=1, which fails the build when the tool is missing. + */ + const resolveSignMode = Effect.fnUntraced(function* () { + if (Bun.which("rcodesign")) { + return "adhoc"; + } + if ((yield* envOrEmpty("SUPABASE_CLI_REQUIRE_SIGNING")) === "1") { + yield* Console.error( + "rcodesign not found but SUPABASE_CLI_REQUIRE_SIGNING=1 is set. Release builds " + + "must sign the macOS binaries. Install rcodesign (https://github.com/indygreg/apple-platform-rs).", + ); + process.exit(1); + } + yield* Console.warn( + "WARNING: rcodesign not found — macOS binaries will keep Bun's linker-signed ad-hoc " + + "signature, which macOS 26+ SIGKILLs at launch (CLI-1621). Install rcodesign to " + + "produce runnable macOS binaries locally.", + ); + return "off"; + }); - console.log(`[${target.pkg}] Creating archive ${target.archive}...`); + const signDarwinBinaries = Effect.fnUntraced(function* (mode: SignMode) { + if (mode === "off") { + return; + } - if (target.archive.endsWith(".zip")) { - const files = [ - path.join(binDir, `supabase${target.ext}`), - path.join(binDir, `supabase-go${target.ext}`), - ]; - await $`zip -j ${archivePath} ${files}`; - - // setup-cli and other download clients always fetch a .tar.gz, even on Windows, so - // publish one alongside the .zip. See #5257. - const tarArchive = target.archive.replace(/\.zip$/, ".tar.gz"); - const tarArchivePath = path.join(distDir, tarArchive); - const tarFiles = [`supabase${target.ext}`, `supabase-go${target.ext}`]; - console.log(`[${target.pkg}] Creating archive ${tarArchive}...`); - await $`tar -czf ${tarArchivePath} -C ${binDir} ${tarFiles}`; - } else { - const files = [`supabase${target.ext}`, `supabase-go${target.ext}`]; - await $`tar -czf ${archivePath} -C ${binDir} ${files}`; - } -} + const darwinTargets = TARGETS.filter((target) => target.bunTarget.startsWith("bun-darwin")); + const binaries = darwinBinaries(); -async function buildMuslBinaries() { - await Promise.all( - MUSL_TARGETS.map(async (target) => { + for (const target of darwinTargets) { const binDir = path.join(root, "packages", target.pkg, "bin"); - await mkdir(binDir, { recursive: true }); - - const outfile = path.join(binDir, "supabase"); - console.log(`[${target.pkg}] Compiling Bun CLI (musl)...`); - await runBunBuild({ - entrypoints: [entrypoint], - compile: { target: target.bunTarget, outfile }, - define: { - ...buildDefines, - SUPABASE_CLI_VERSION: JSON.stringify(version), - }, - }); - - // The Go binary is fully static (CGO_ENABLED=0), so the glibc build works on musl too; - // copy it into the musl package so GoProxy finds supabase-go there. - const glibcTarget = TARGETS.find( - (candidate) => "nfpmArch" in candidate && candidate.nfpmArch === target.nfpmArch, - ); - if (!glibcTarget) { - throw new Error(`No glibc Linux target found for musl arch ${target.nfpmArch}`); + + for (const binary of binaries) { + const binPath = path.join(binDir, binary); + const identifier = MACOS_IDENTIFIERS[binary]; + + yield* Console.log(`[${target.pkg}] Ad-hoc signing ${binary} (${identifier})...`); + // No key material, so rcodesign produces an ad-hoc signature, equivalent to + // `codesign --sign -`, replacing Bun/Go's linker-signed one. + yield* step(() => $`rcodesign sign --binary-identifier ${identifier} ${binPath}`); + + // Matches the identifier's whole value, so the SFE's `com.supabase.cli` can't satisfy + // the sidecar's `com.supabase.cli-go`, and confirms the signature is no longer linker-signed. + const info = yield* step(() => $`rcodesign print-signature-info ${binPath}`.text()); + const signedIdentifier = info.match(/^\s*identifier:\s*(\S+)\s*$/m)?.[1]; + if (signedIdentifier !== identifier) { + throw new Error( + `Signing ${binPath} failed: expected identifier ${identifier}, got ${signedIdentifier ?? "none"}`, + ); + } + if (info.includes("LINKER_SIGNED")) { + throw new Error(`Signing ${binPath} failed: signature is still linker-signed`); + } } - const src = path.join(root, "packages", glibcTarget.pkg, "bin", "supabase-go"); - const dst = path.join(binDir, "supabase-go"); - console.log(`[${target.pkg}] Copying Go binary from ${glibcTarget.pkg}...`); - await copyFile(src, dst); + } + }); - console.log(`[${target.pkg}] Done.`); - }), - ); -} + const archiveTarget = Effect.fnUntraced(function* (target: (typeof TARGETS)[number]) { + const binDir = path.join(root, "packages", target.pkg, "bin"); + const archivePath = path.join(distDir, target.archive); -async function buildLinuxPackages(version: string) { - const linuxTargets = TARGETS.filter((target) => "nfpmArch" in target); - const jobs: Promise[] = []; - - for (const target of linuxTargets) { - const glibcBinDir = path.join(root, "packages", target.pkg, "bin"); - const muslTarget = MUSL_TARGETS.find((candidate) => candidate.nfpmArch === target.nfpmArch)!; - const muslBinDir = path.join(root, "packages", muslTarget.pkg, "bin"); - - for (const fmt of LINUX_PKG_FORMATS) { - const outFile = `supabase_${version}_linux_${target.nfpmArch}.${fmt}`; - const outPath = path.join(distDir, outFile); - const binDir = fmt === "apk" ? muslBinDir : glibcBinDir; - - // The Go binary is fully static, so apk (musl) still references supabase-go - // from the glibc dir where it was built. - const contents: Array<{ src: string; dst: string }> = [ - { src: path.join(binDir, "supabase"), dst: "/usr/bin/supabase" }, - { src: path.join(glibcBinDir, "supabase-go"), dst: "/usr/bin/supabase-go" }, + yield* Console.log(`[${target.pkg}] Creating archive ${target.archive}...`); + + if (target.archive.endsWith(".zip")) { + const files = [ + path.join(binDir, `supabase${target.ext}`), + path.join(binDir, `supabase-go${target.ext}`), ]; + yield* step(() => $`zip -j ${archivePath} ${files}`); - const nfpmConfig: Record = { - name: "supabase", - arch: target.nfpmArch, - platform: "linux", - version, - maintainer: "Supabase ", - description: "Supabase CLI", - homepage: "https://supabase.com", - license: "MIT", - contents, - }; - - if (fmt === "apk") { - nfpmConfig.depends = ["libstdc++", "libgcc"]; - } + // setup-cli and other download clients always fetch a .tar.gz, even on Windows, so + // publish one alongside the .zip. See #5257. + const tarArchive = target.archive.replace(/\.zip$/, ".tar.gz"); + const tarArchivePath = path.join(distDir, tarArchive); + const tarFiles = [`supabase${target.ext}`, `supabase-go${target.ext}`]; + yield* Console.log(`[${target.pkg}] Creating archive ${tarArchive}...`); + yield* step(() => $`tar -czf ${tarArchivePath} -C ${binDir} ${tarFiles}`); + } else { + const files = [`supabase${target.ext}`, `supabase-go${target.ext}`]; + yield* step(() => $`tar -czf ${archivePath} -C ${binDir} ${files}`); + } + }); - const configPath = path.join(distDir, `nfpm-${target.nfpmArch}-${fmt}.yaml`); - await writeFile(configPath, JSON.stringify(nfpmConfig)); + const buildMuslBinaries = Effect.fnUntraced(function* () { + yield* step(() => + Promise.all( + MUSL_TARGETS.map((target) => + runJob( + Effect.gen(function* () { + const binDir = path.join(root, "packages", target.pkg, "bin"); + yield* fs.makeDirectory(binDir, { recursive: true }); + + const outfile = path.join(binDir, "supabase"); + yield* Console.log(`[${target.pkg}] Compiling Bun CLI (musl)...`); + yield* runBunBuild({ + entrypoints: [entrypoint], + compile: { target: target.bunTarget, outfile }, + define: { + ...buildDefines, + SUPABASE_CLI_VERSION: yield* encodeJson(version), + }, + }); + + // The Go binary is fully static (CGO_ENABLED=0), so the glibc build works on musl too; + // copy it into the musl package so GoProxy finds supabase-go there. + const glibcTarget = TARGETS.find( + (candidate) => "nfpmArch" in candidate && candidate.nfpmArch === target.nfpmArch, + ); + if (!glibcTarget) { + throw new Error(`No glibc Linux target found for musl arch ${target.nfpmArch}`); + } + const src = path.join(root, "packages", glibcTarget.pkg, "bin", "supabase-go"); + const dst = path.join(binDir, "supabase-go"); + yield* Console.log(`[${target.pkg}] Copying Go binary from ${glibcTarget.pkg}...`); + yield* fs.copyFile(src, dst); + + yield* Console.log(`[${target.pkg}] Done.`); + }), + ), + ), + ), + ); + }); - jobs.push( - (async () => { - console.log(`[${target.pkg}] Creating ${outFile}...`); - await $`nfpm package --config ${configPath} --packager ${fmt} --target ${outPath}`; - await rm(configPath); - })(), - ); + const buildLinuxPackages = Effect.fnUntraced(function* (version: string) { + const linuxTargets = TARGETS.filter((target) => "nfpmArch" in target); + const jobs: Promise[] = []; + + for (const target of linuxTargets) { + const glibcBinDir = path.join(root, "packages", target.pkg, "bin"); + const muslTarget = MUSL_TARGETS.find((candidate) => candidate.nfpmArch === target.nfpmArch)!; + const muslBinDir = path.join(root, "packages", muslTarget.pkg, "bin"); + + for (const fmt of LINUX_PKG_FORMATS) { + const outFile = `supabase_${version}_linux_${target.nfpmArch}.${fmt}`; + const outPath = path.join(distDir, outFile); + const binDir = fmt === "apk" ? muslBinDir : glibcBinDir; + + // The Go binary is fully static, so apk (musl) still references supabase-go + // from the glibc dir where it was built. + const contents: Array<{ src: string; dst: string }> = [ + { src: path.join(binDir, "supabase"), dst: "/usr/bin/supabase" }, + { src: path.join(glibcBinDir, "supabase-go"), dst: "/usr/bin/supabase-go" }, + ]; + + const nfpmConfig: Record = { + name: "supabase", + arch: target.nfpmArch, + platform: "linux", + version, + maintainer: "Supabase ", + description: "Supabase CLI", + homepage: "https://supabase.com", + license: "MIT", + contents, + }; + + if (fmt === "apk") { + nfpmConfig.depends = ["libstdc++", "libgcc"]; + } + + const configPath = path.join(distDir, `nfpm-${target.nfpmArch}-${fmt}.yaml`); + yield* fs.writeFileString(configPath, yield* encodeJson(nfpmConfig)); + + jobs.push( + runJob( + Effect.gen(function* () { + yield* Console.log(`[${target.pkg}] Creating ${outFile}...`); + yield* step( + () => $`nfpm package --config ${configPath} --packager ${fmt} --target ${outPath}`, + ); + yield* fs.remove(configPath); + }), + ), + ); + } } - } - await Promise.all(jobs); -} + yield* step(() => Promise.all(jobs)); + }); -async function generateChecksums() { - const lines: string[] = []; + const generateChecksums = Effect.fnUntraced(function* () { + const lines: string[] = []; - for (const target of TARGETS) { - const archivePath = path.join(distDir, target.archive); - const data = await readFile(archivePath); - const hash = createHash("sha256").update(data).digest("hex"); - lines.push(`${hash} ${target.archive}`); + for (const target of TARGETS) { + const archivePath = path.join(distDir, target.archive); + const data = yield* fs.readFile(archivePath); + const hash = createHash("sha256").update(data).digest("hex"); + lines.push(`${hash} ${target.archive}`); - if (target.archive.endsWith(".zip")) { - const tarArchive = target.archive.replace(/\.zip$/, ".tar.gz"); - const tarData = await readFile(path.join(distDir, tarArchive)); - const tarHash = createHash("sha256").update(tarData).digest("hex"); - lines.push(`${tarHash} ${tarArchive}`); + if (target.archive.endsWith(".zip")) { + const tarArchive = target.archive.replace(/\.zip$/, ".tar.gz"); + const tarData = yield* fs.readFile(path.join(distDir, tarArchive)); + const tarHash = createHash("sha256").update(tarData).digest("hex"); + lines.push(`${tarHash} ${tarArchive}`); + } } - } - const linuxTargets = TARGETS.filter((target) => "nfpmArch" in target); - for (const target of linuxTargets) { - for (const fmt of LINUX_PKG_FORMATS) { - const filename = `supabase_${version}_linux_${target.nfpmArch}.${fmt}`; - const data = await readFile(path.join(distDir, filename)); - const hash = createHash("sha256").update(data).digest("hex"); - lines.push(`${hash} ${filename}`); + const linuxTargets = TARGETS.filter((target) => "nfpmArch" in target); + for (const target of linuxTargets) { + for (const fmt of LINUX_PKG_FORMATS) { + const filename = `supabase_${version}_linux_${target.nfpmArch}.${fmt}`; + const data = yield* fs.readFile(path.join(distDir, filename)); + const hash = createHash("sha256").update(data).digest("hex"); + lines.push(`${hash} ${filename}`); + } } - } - const checksumsPath = path.join(distDir, "checksums.txt"); - await writeFile(checksumsPath, `${lines.join("\n")}\n`); - console.log("Checksums written to dist/checksums.txt"); -} + const checksumsPath = path.join(distDir, "checksums.txt"); + yield* fs.writeFileString(checksumsPath, `${lines.join("\n")}\n`); + yield* Console.log("Checksums written to dist/checksums.txt"); + }); + + yield* Console.log(`Building the CLI for ${TARGETS.length} targets...\n`); + + yield* step(() => Promise.all(TARGETS.map((target) => runJob(buildTarget(target))))); -console.log(`Building the CLI for ${TARGETS.length} targets...\n`); + yield* Console.log("\nCompiling Go CLI for all targets..."); + yield* step(() => Promise.all(TARGETS.map((target) => runJob(buildGoTarget(target))))); -await Promise.all(TARGETS.map(buildTarget)); + // Must run before archiveTarget / buildLinuxPackages / generateChecksums so every + // distribution channel ships the signed bytes. + const signMode = yield* resolveSignMode(); + yield* Console.log(`\nSigning macOS binaries (mode: ${signMode})...`); + yield* signDarwinBinaries(signMode); -console.log("\nCompiling Go CLI for all targets..."); -await Promise.all(TARGETS.map(buildGoTarget)); + yield* fs.makeDirectory(distDir, { recursive: true }); + yield* step(() => Promise.all(TARGETS.map((target) => runJob(archiveTarget(target))))); -// Must run before archiveTarget / buildLinuxPackages / generateChecksums so every -// distribution channel ships the signed bytes. -const signMode = resolveSignMode(); -console.log(`\nSigning macOS binaries (mode: ${signMode})...`); -await signDarwinBinaries(signMode); + yield* buildMuslBinaries(); + yield* buildLinuxPackages(version); + yield* generateChecksums(); -await mkdir(distDir, { recursive: true }); -await Promise.all(TARGETS.map(archiveTarget)); + yield* Console.log("\nAll targets built successfully."); +}); -await buildMuslBinaries(); -await buildLinuxPackages(version); -await generateChecksums(); +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + return error.cause !== undefined ? error.cause : new Error(error.message); +} -console.log("\nAll targets built successfully."); +await Effect.runPromise( + main.pipe( + Effect.catch((error) => Effect.die(thrownValue(error))), + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + ), +); diff --git a/apps/cli/scripts/compile-options.ts b/apps/cli/scripts/compile-options.ts index b18cf9a2e2..93797ca83c 100644 --- a/apps/cli/scripts/compile-options.ts +++ b/apps/cli/scripts/compile-options.ts @@ -20,8 +20,7 @@ export const compileOptions = { * Embeds the stack release of the sources being compiled, so the binary drives exactly the owners * started from the same stack sources, whether compiled or run from source. */ -export const stackReleaseDefine = async () => ({ - SUPABASE_STACK_BUILD_ID: JSON.stringify( - await Effect.runPromise(stackSourceDigest.pipe(Effect.provide(BunServices.layer))), - ), -}); +export const stackReleaseDefine = () => + Effect.runPromise(stackSourceDigest.pipe(Effect.provide(BunServices.layer))).then((digest) => ({ + SUPABASE_STACK_BUILD_ID: JSON.stringify(digest), + })); diff --git a/apps/cli/scripts/detect-unmirrored-images.ts b/apps/cli/scripts/detect-unmirrored-images.ts index 5bf3319f12..2b69f24c3c 100644 --- a/apps/cli/scripts/detect-unmirrored-images.ts +++ b/apps/cli/scripts/detect-unmirrored-images.ts @@ -1,9 +1,21 @@ // Detects images pinned in apps/cli/src/shared/services/Dockerfile that are missing from // any mirror registry and emits them as JSON, for the mirror-template-images workflow's backfill // matrix. -import { spawnSync } from "node:child_process"; -import { appendFileSync } from "node:fs"; -import process from "node:process"; +import { constants } from "node:os"; +import { BunServices } from "@effect/platform-bun"; +import { + Config, + Console, + Data, + Deferred, + Effect, + FileSystem, + Predicate, + Schema, + Semaphore, + Stream, +} from "effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; import { dockerfileServiceImages } from "../src/shared/services/dockerfile-images.ts"; /** @@ -43,26 +55,27 @@ export interface MirrorPartition { * any one registry lands in `missing`, since the backfill must re-push it everywhere; images * already mirrored everywhere land in `mirrored` and are skipped by a re-run. */ -export async function partitionUnmirroredImages( +export const partitionUnmirroredImages = ( images: Iterable, - isMirrored: (target: string) => Promise, + isMirrored: (target: string) => Effect.Effect, registries: ReadonlyArray = MIRROR_REGISTRIES, -): Promise { - const unique = [...new Set(images)]; - const results = await Promise.all( - unique.map(async (image) => { - const presence = await Promise.all( - mirrorImageTargets(image, registries).map((target) => isMirrored(target)), - ); - return { image, mirrored: presence.every(Boolean) }; - }), - ); +): Effect.Effect => + Effect.gen(function* () { + const unique = [...new Set(images)]; + const results = yield* Effect.forEach( + unique, + (image) => + Effect.forEach(mirrorImageTargets(image, registries), (target) => isMirrored(target), { + concurrency: "unbounded", + }).pipe(Effect.map((presence) => ({ image, mirrored: presence.every(Boolean) }))), + { concurrency: "unbounded" }, + ); - return { - mirrored: results.filter((result) => result.mirrored).map((result) => result.image), - missing: results.filter((result) => !result.mirrored).map((result) => result.image), - }; -} + return { + mirrored: results.filter((result) => result.mirrored).map((result) => result.image), + missing: results.filter((result) => !result.mirrored).map((result) => result.image), + }; + }); /** * Interprets a `docker buildx imagetools inspect` result. Only a registry "not found" means the @@ -91,66 +104,222 @@ export interface InspectResult { /** Backoff before each retry of a rate-limited (HTTP 429 or OCI `toomanyrequests`) registry probe. */ export const RATE_LIMIT_RETRY_DELAYS_MS: ReadonlyArray = [10_000, 30_000, 60_000]; +class MirrorInspectError extends Data.TaggedError("MirrorInspectError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + /** * Runs a registry probe, retrying with backoff while the registry rate-limits it (HTTP 429 or OCI * `toomanyrequests`); the anonymous public.ecr.aws quota is shared per runner IP and usually * recovers within a minute. */ -export async function inspectWithRateLimitRetry( +export const inspectWithRateLimitRetry = ( target: string, - inspect: () => InspectResult, - sleep: (ms: number) => Promise, + inspect: Effect.Effect, + sleep: (ms: number) => Effect.Effect, delaysMs: ReadonlyArray = RATE_LIMIT_RETRY_DELAYS_MS, -): Promise { - for (let attempt = 0; ; attempt++) { - const { exit, stderr } = inspect(); - const delay = delaysMs[attempt]; - if ( - exit !== 0 && - /429 Too Many Requests|toomanyrequests/i.test(stderr) && - delay !== undefined - ) { - console.error(`rate limited inspecting ${target}, retrying in ${delay}ms`); - await sleep(delay); - continue; +): Effect.Effect => + Effect.gen(function* () { + for (let attempt = 0; ; attempt++) { + const { exit, stderr } = yield* inspect; + const delay = delaysMs[attempt]; + if ( + exit !== 0 && + /429 Too Many Requests|toomanyrequests/i.test(stderr) && + delay !== undefined + ) { + yield* Console.error(`rate limited inspecting ${target}, retrying in ${delay}ms`); + yield* sleep(delay); + continue; + } + return yield* Effect.try({ + try: () => inspectResultIsMirrored(target, exit, stderr), + catch: (cause) => + new MirrorInspectError({ + message: cause instanceof Error ? cause.message : String(cause), + cause, + }), + }); } - return inspectResultIsMirrored(target, exit, stderr); - } -} + }); -function imageExistsOnMirror(target: string): Promise { - return inspectWithRateLimitRetry( - target, - () => { - const result = spawnSync("docker", ["buildx", "imagetools", "inspect", target], { - encoding: "utf8", - stdio: ["ignore", "ignore", "pipe"], - }); - if (result.error) { - throw result.error; - } - return { exit: result.status ?? result.signal, stderr: result.stderr }; +const platformMessage = (error: { readonly message: string; readonly cause?: unknown }) => + error.cause instanceof Error ? error.cause.message : error.message; + +const isSignal = (name: string): name is NodeJS.Signals => Object.hasOwn(constants.signals, name); + +const receivedSignal = (error: { readonly message: string; readonly cause?: unknown }) => { + const name = /^Process interrupted due to receipt of signal: '(.+)'$/.exec( + platformMessage(error), + )?.[1]; + return name !== undefined && isSignal(name) ? name : undefined; +}; + +const MAX_STDERR_BYTES = 1024 * 1024; + +function stderrOverflowError(args: ReadonlyArray): Error { + return Object.assign( + new Error("spawnSync docker ENOBUFS (stdout or stderr buffer reached maxBuffer size limit)"), + { + path: "docker", + syscall: "spawnSync docker", + errno: -constants.errno.ENOBUFS, + code: "ENOBUFS", + spawnargs: [...args], }, - (ms) => new Promise((resolve) => setTimeout(resolve, ms)), ); } -if (import.meta.main) { +/* Decodes the whole stderr at once and keeps a leading byte order mark. */ +const utf8 = new TextDecoder("utf-8", { ignoreBOM: true }); + +const inspectMirrorTarget = (target: string) => + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const args = ["buildx", "imagetools", "inspect", target]; + const handle = yield* spawner + .spawn( + ChildProcess.make("docker", args, { + detached: false, + stdin: "ignore", + stdout: "ignore", + stderr: "pipe", + }), + ) + .pipe( + /* Spawn errors keep the `spawnSync docker` syscall label. */ + Effect.tapError((error) => + Effect.sync(() => { + if ( + Predicate.hasProperty(error.cause, "syscall") && + error.cause.syscall === "spawn docker" + ) { + error.cause.syscall = "spawnSync docker"; + } + }), + ), + Effect.catchDefect((defect) => + Effect.fail( + new MirrorInspectError({ + message: defect instanceof Error ? defect.message : String(defect), + cause: defect, + }), + ), + ), + ); + const chunks: Array = []; + let stderrBytes = 0; + const readStderr = Stream.runForEach(handle.stderr, (chunk) => + Effect.suspend(() => { + stderrBytes += chunk.byteLength; + if (stderrBytes > MAX_STDERR_BYTES) { + const cause = stderrOverflowError(args); + return Effect.fail(new MirrorInspectError({ message: cause.message, cause })); + } + chunks.push(chunk); + return Effect.void; + }), + ).pipe( + Effect.map(() => { + const bytes = new Uint8Array(stderrBytes); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + return utf8.decode(bytes); + }), + ); + const [stderr, exit] = yield* Effect.all( + [ + readStderr, + handle.exitCode.pipe( + Effect.map((code): number | NodeJS.Signals => Number(code)), + Effect.catch((error) => { + const signal = receivedSignal(error); + return signal === undefined ? Effect.fail(error) : Effect.succeed(signal); + }), + ), + ], + { concurrency: "unbounded" }, + ); + return { exit, stderr } satisfies InspectResult; + }).pipe( + Effect.scoped, + Effect.catchTag("PlatformError", (error) => + Effect.fail(new MirrorInspectError({ message: platformMessage(error), cause: error.cause })), + ), + ); + +const encodeMissing = Schema.encodeEffect(Schema.fromJsonString(Schema.Array(Schema.String))); + +const main = Effect.gen(function* () { + /* Probes run one at a time, as the registries rate-limit bursts; a rate-limit backoff does not + hold the permit, so other probes continue meanwhile. */ + const probe = yield* Semaphore.make(1); const images = dockerfileServiceImages.map((spec) => spec.image); - const { mirrored, missing } = await partitionUnmirroredImages(images, imageExistsOnMirror); + + /* Every target gets its first probe before a probe failure aborts detection, and the earliest + failure is the one reported. */ + let firstProbesLeft = new Set(images).size * MIRROR_REGISTRIES.length; + const firstProbesDone = yield* Deferred.make(); + if (firstProbesLeft === 0) { + yield* Deferred.succeed(firstProbesDone, undefined); + } + const probeFailure = yield* Deferred.make(); + const imageExistsOnMirror = (target: string) => + Effect.suspend(() => { + let firstProbe = true; + const inspect = probe.withPermit(inspectMirrorTarget(target)).pipe( + Effect.ensuring( + Effect.suspend(() => { + if (!firstProbe) { + return Effect.void; + } + firstProbe = false; + firstProbesLeft -= 1; + return firstProbesLeft === 0 + ? Deferred.succeed(firstProbesDone, undefined) + : Effect.void; + }), + ), + ); + return inspectWithRateLimitRetry(target, inspect, (ms) => Effect.sleep(ms)).pipe( + Effect.catch((error) => + Deferred.fail(probeFailure, error).pipe(Effect.andThen(Effect.never)), + ), + ); + }); + + const { mirrored, missing } = yield* Effect.raceFirst( + partitionUnmirroredImages(images, imageExistsOnMirror), + Deferred.await(firstProbesDone).pipe(Effect.andThen(Deferred.await(probeFailure))), + ); for (const image of mirrored) { - console.error(`already mirrored: ${image}`); + yield* Console.error(`already mirrored: ${image}`); } for (const image of missing) { - console.error(`needs mirror: ${image} -> ${mirrorImageTargets(image).join(", ")}`); + yield* Console.error(`needs mirror: ${image} -> ${mirrorImageTargets(image).join(", ")}`); } - const json = JSON.stringify(missing); - console.log(json); + const json = yield* encodeMissing(missing); + yield* Console.log(json); // Expose the list to the workflow as a step output when running in CI. - if (process.env.GITHUB_OUTPUT) { - appendFileSync(process.env.GITHUB_OUTPUT, `missing=${json}\n`); + const githubOutput = yield* Config.String("GITHUB_OUTPUT").pipe(Config.withDefault("")); + if (githubOutput) { + const fs = yield* FileSystem.FileSystem; + yield* fs.writeFileString(githubOutput, `missing=${json}\n`, { flag: "a" }); } +}); + +if (import.meta.main) { + await Effect.runPromise( + main.pipe( + Effect.mapError((error) => error.cause ?? error), + Effect.provide(BunServices.layer), + ), + ); } diff --git a/apps/cli/scripts/detect-unmirrored-images.unit.test.ts b/apps/cli/scripts/detect-unmirrored-images.unit.test.ts index 07b699f80a..00a43b0f15 100644 --- a/apps/cli/scripts/detect-unmirrored-images.unit.test.ts +++ b/apps/cli/scripts/detect-unmirrored-images.unit.test.ts @@ -1,3 +1,5 @@ +import { it } from "@effect/vitest"; +import { Effect } from "effect"; import { describe, expect, test } from "vitest"; import { inspectResultIsMirrored, @@ -26,41 +28,46 @@ describe("detect unmirrored images", () => { ]); }); - test("an image is mirrored only when present on ALL registries", async () => { - const present = new Set([ - // kong is on both registries -> mirrored. - "public.ecr.aws/supabase/kong:2.8.1", - "ghcr.io/supabase/kong:2.8.1", - // postgrest is only on ghcr.io -> partial mirror, must be re-pushed. - "ghcr.io/supabase/postgrest:v14.14", - ]); - const queried: string[] = []; - const isMirrored = (target: string) => { - queried.push(target); - return Promise.resolve(present.has(target)); - }; + it.effect("an image is mirrored only when present on ALL registries", () => + Effect.gen(function* () { + const present = new Set([ + // kong is on both registries -> mirrored. + "public.ecr.aws/supabase/kong:2.8.1", + "ghcr.io/supabase/kong:2.8.1", + // postgrest is only on ghcr.io -> partial mirror, must be re-pushed. + "ghcr.io/supabase/postgrest:v14.14", + ]); + const queried: string[] = []; + const isMirrored = (target: string) => + Effect.sync(() => { + queried.push(target); + return present.has(target); + }); - const { mirrored, missing } = await partitionUnmirroredImages( - // Duplicate kong to prove de-duplication. - ["library/kong:2.8.1", "postgrest/postgrest:v14.14", "library/kong:2.8.1"], - isMirrored, - ); + const { mirrored, missing } = yield* partitionUnmirroredImages( + // Duplicate kong to prove de-duplication. + ["library/kong:2.8.1", "postgrest/postgrest:v14.14", "library/kong:2.8.1"], + isMirrored, + ); - expect(mirrored).toEqual(["library/kong:2.8.1"]); - expect(missing).toEqual(["postgrest/postgrest:v14.14"]); - expect(queried).toHaveLength(4); - }); + expect(mirrored).toEqual(["library/kong:2.8.1"]); + expect(missing).toEqual(["postgrest/postgrest:v14.14"]); + expect(queried).toHaveLength(4); + }), + ); - test("is a no-op once everything is on every registry (idempotent re-run)", async () => { - const allMirrored = () => Promise.resolve(true); - const { mirrored, missing } = await partitionUnmirroredImages( - ["postgrest/postgrest:v14.14", "supabase/logflare:1.45.6"], - allMirrored, - ); + it.effect("is a no-op once everything is on every registry (idempotent re-run)", () => + Effect.gen(function* () { + const allMirrored = () => Effect.succeed(true); + const { mirrored, missing } = yield* partitionUnmirroredImages( + ["postgrest/postgrest:v14.14", "supabase/logflare:1.45.6"], + allMirrored, + ); - expect(missing).toEqual([]); - expect(mirrored).toEqual(["postgrest/postgrest:v14.14", "supabase/logflare:1.45.6"]); - }); + expect(missing).toEqual([]); + expect(mirrored).toEqual(["postgrest/postgrest:v14.14", "supabase/logflare:1.45.6"]); + }), + ); test("only a registry not-found marks an image as unmirrored", () => { const target = "public.ecr.aws/supabase/realtime:v2.144.2"; @@ -79,27 +86,37 @@ describe("detect unmirrored images", () => { ); }); - test("a rate-limited probe is retried with backoff and only aborts once retries run out", async () => { - const target = "public.ecr.aws/supabase/postgres:17.11.0.006"; - const rateLimited = { - exit: 1, - stderr: "ERROR: httpReadSeeker: failed open: unexpected status: 429 Too Many Requests\n", - }; - const slept: number[] = []; - const sleep = (ms: number) => { - slept.push(ms); - return Promise.resolve(); - }; + it.effect( + "a rate-limited probe is retried with backoff and only aborts once retries run out", + () => + Effect.gen(function* () { + const target = "public.ecr.aws/supabase/postgres:17.11.0.006"; + const rateLimited = { + exit: 1, + stderr: "ERROR: httpReadSeeker: failed open: unexpected status: 429 Too Many Requests\n", + }; + const slept: number[] = []; + const sleep = (ms: number) => + Effect.sync(() => { + slept.push(ms); + }); - const quotaExceeded = { exit: 1, stderr: "ERROR: toomanyrequests: Data limit exceeded\n" }; - const responses = [rateLimited, quotaExceeded, { exit: 0, stderr: "" }]; - await expect( - inspectWithRateLimitRetry(target, () => responses.shift()!, sleep, [1, 2, 3]), - ).resolves.toBe(true); - expect(slept).toEqual([1, 2]); + const quotaExceeded = { exit: 1, stderr: "ERROR: toomanyrequests: Data limit exceeded\n" }; + const responses = [rateLimited, quotaExceeded, { exit: 0, stderr: "" }]; + expect( + yield* inspectWithRateLimitRetry( + target, + Effect.sync(() => responses.shift()!), + sleep, + [1, 2, 3], + ), + ).toBe(true); + expect(slept).toEqual([1, 2]); - await expect(inspectWithRateLimitRetry(target, () => rateLimited, sleep, [1])).rejects.toThrow( - "429 Too Many Requests", - ); - }); + const error = yield* Effect.flip( + inspectWithRateLimitRetry(target, Effect.succeed(rateLimited), sleep, [1]), + ); + expect(error.message).toContain("429 Too Many Requests"); + }), + ); }); diff --git a/apps/cli/scripts/generate-docs-spec.ts b/apps/cli/scripts/generate-docs-spec.ts index 83530c0455..9f85e9c2b4 100644 --- a/apps/cli/scripts/generate-docs-spec.ts +++ b/apps/cli/scripts/generate-docs-spec.ts @@ -8,8 +8,7 @@ * version is a semantic-release placeholder and is never used here. */ import { BunServices } from "@effect/platform-bun"; -import { Effect } from "effect"; -import path from "node:path"; +import { Effect, Path } from "effect"; import process from "node:process"; import { readDocsContent } from "../src/docs/docs-spec.content.ts"; import { buildDocsSpec, stringifyDocsSpec } from "../src/docs/docs-spec.ts"; @@ -22,7 +21,8 @@ function resolveVersion(): string { } const content = await Effect.runPromise( - readDocsContent(path.resolve(import.meta.dir, "../docs")).pipe( + Path.Path.pipe( + Effect.flatMap((path) => readDocsContent(path.resolve(import.meta.dir, "../docs"))), Effect.provide(BunServices.layer), Effect.mapError((error) => error.cause ?? error), ), diff --git a/apps/cli/scripts/generate-docs.ts b/apps/cli/scripts/generate-docs.ts index e359d5ca00..886f0866ce 100644 --- a/apps/cli/scripts/generate-docs.ts +++ b/apps/cli/scripts/generate-docs.ts @@ -1,118 +1,144 @@ -import { copyFileSync, mkdirSync, writeFileSync } from "node:fs"; -import path from "node:path"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import process from "node:process"; import { CLI_CONFIG_SCHEMA_URL, PROJECT_CONFIG_SCHEMA_URL } from "@supabase/config"; import { Lock, LOCK_SCHEMA_URL } from "@supabase/stack/effect"; -import { Schema } from "effect"; +import { Console, Effect, FileSystem, Layer, Path, Schema } from "effect"; import { rootCommand } from "../src/cli/root.ts"; import { collectCommands, getHelpDoc } from "../src/shared/cli/command-docs.ts"; import { formatHelpDocAsMarkdown } from "../src/shared/cli/markdown-formatter.ts"; const BINARY_NAME = "supabase"; -const defaultContentDir = path.resolve(import.meta.dir, "../../../apps/docs/content/docs/commands"); -const defaultDocsPublicDir = path.resolve(import.meta.dir, "../../../apps/docs/public"); -const configPackageDistDir = path.resolve(import.meta.dir, "../../../packages/config/dist"); -const contentDir = process.argv[2] - ? path.resolve(process.cwd(), process.argv[2]) - : defaultContentDir; - -function generateCommandDocs() { - const leaves = collectCommands(rootCommand, [BINARY_NAME]).filter( - ({ command, commandPath }) => commandPath.length > 1 && command.subcommands.length === 0, +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 })); + +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const defaultContentDir = path.resolve( + import.meta.dir, + "../../../apps/docs/content/docs/commands", ); - - const pages: Array<{ slug: string; title: string; description: string }> = []; - - for (const { command, commandPath } of leaves) { - const helpDoc = getHelpDoc(command, commandPath); - const body = formatHelpDocAsMarkdown(helpDoc); - - const title = commandPath.slice(1).join(" "); - const description = - (command as any).shortDescription ?? helpDoc.description?.split("\n")[0] ?? ""; - - const slug = commandPath.slice(1).join("/"); - const frontmatter = [ + const defaultDocsPublicDir = path.resolve(import.meta.dir, "../../../apps/docs/public"); + const configPackageDistDir = path.resolve(import.meta.dir, "../../../packages/config/dist"); + const contentDir = process.argv[2] + ? path.resolve(process.cwd(), process.argv[2]) + : defaultContentDir; + + const generateCommandDocs = Effect.fnUntraced(function* () { + const leaves = collectCommands(rootCommand, [BINARY_NAME]).filter( + ({ command, commandPath }) => commandPath.length > 1 && command.subcommands.length === 0, + ); + + const pages: Array<{ slug: string; title: string; description: string }> = []; + + for (const { command, commandPath } of leaves) { + const helpDoc = getHelpDoc(command, commandPath); + const body = formatHelpDocAsMarkdown(helpDoc); + + const title = commandPath.slice(1).join(" "); + const description = + (command as any).shortDescription ?? helpDoc.description?.split("\n")[0] ?? ""; + + const slug = commandPath.slice(1).join("/"); + const frontmatter = [ + "---", + `title: "${BINARY_NAME} ${title}"`, + `description: "${description.replace(/"/g, '\\"')}"`, + "---", + ].join("\n"); + + const mdxContent = `${frontmatter}\n\n${body}`; + + const filePath = path.join(contentDir, `${slug}.mdx`); + yield* fs.makeDirectory(path.dirname(filePath), { recursive: true }); + yield* fs.writeFileString(filePath, mdxContent); + pages.push({ slug, title, description }); + + yield* Console.log(`Generated: commands/${slug}.mdx`); + } + + const indexFrontmatter = [ "---", - `title: "${BINARY_NAME} ${title}"`, - `description: "${description.replace(/"/g, '\\"')}"`, + "title: Command reference", + "description: Complete reference for all Supabase CLI commands", "---", ].join("\n"); - const mdxContent = `${frontmatter}\n\n${body}`; - - const filePath = path.join(contentDir, `${slug}.mdx`); - mkdirSync(path.dirname(filePath), { recursive: true }); - writeFileSync(filePath, mdxContent); - pages.push({ slug, title, description }); - - console.log(`Generated: commands/${slug}.mdx`); - } - - const indexFrontmatter = [ - "---", - "title: Command reference", - "description: Complete reference for all Supabase CLI commands", - "---", - ].join("\n"); - - const rows = pages.map( - (page) => - `| [\`${BINARY_NAME} ${page.title}\`](/docs/commands/${page.slug}) | ${page.description} |`, - ); - const table = `| Command | Description |\n| --- | --- |\n${rows.join("\n")}`; - const indexContent = `${indexFrontmatter}\n\n${table}\n`; - - writeFileSync(path.join(contentDir, "index.mdx"), indexContent); - console.log("Generated: commands/index.mdx"); - - const metaContent = { - title: "Commands", - pages: ["index", ...pages.map((page) => page.slug.split("/").pop())], - }; - writeFileSync(path.join(contentDir, "meta.json"), JSON.stringify(metaContent, null, 2)); - - console.log(`\nGenerated ${pages.length} command page(s)`); -} - -/** - * Copies `@supabase/config`'s post-processed `dist/*.json` schema artifact to its docs-site - * public path rather than re-rendering it here, since re-rendering would bypass - * `json-schema-postprocess.ts` and produce a mismatched `$id`. Requires - * `@supabase/config#build` to have already run (wired via `turbo.json`). - */ -function copyConfigSchemaAsset(schemaUrl: string, distFileName: string) { - const schemaPathname = new URL(schemaUrl).pathname.replace(/^\/docs/, ""); - const destPath = path.join(defaultDocsPublicDir, schemaPathname); - const sourcePath = path.join(configPackageDistDir, distFileName); - - mkdirSync(path.dirname(destPath), { recursive: true }); - copyFileSync(sourcePath, destPath); - - console.log(`Generated: ${path.relative(path.resolve(import.meta.dir, "../../.."), destPath)}`); -} - -/** Writes the `supabase.lock` v1 JSON Schema, generated from the stack's lock schema, to its docs-site public path. */ -function writeLockSchemaAsset() { - const { schema, definitions } = Schema.toJsonSchemaDocument(Lock); - const document = { - $schema: "https://json-schema.org/draft/2020-12/schema", - $id: LOCK_SCHEMA_URL, - title: "supabase.lock", - description: "The service versions that supabase start pins in supabase/supabase.lock.", - ...schema, - $defs: definitions, - }; - const schemaPathname = new URL(LOCK_SCHEMA_URL).pathname.replace(/^\/docs/, ""); - const destPath = path.join(defaultDocsPublicDir, schemaPathname); - - mkdirSync(path.dirname(destPath), { recursive: true }); - writeFileSync(destPath, `${JSON.stringify(document, null, 2)}\n`); - - console.log(`Generated: ${path.relative(path.resolve(import.meta.dir, "../../.."), destPath)}`); + const rows = pages.map( + (page) => + `| [\`${BINARY_NAME} ${page.title}\`](/docs/commands/${page.slug}) | ${page.description} |`, + ); + const table = `| Command | Description |\n| --- | --- |\n${rows.join("\n")}`; + const indexContent = `${indexFrontmatter}\n\n${table}\n`; + + yield* fs.writeFileString(path.join(contentDir, "index.mdx"), indexContent); + yield* Console.log("Generated: commands/index.mdx"); + + const metaContent = { + title: "Commands", + pages: ["index", ...pages.map((page) => page.slug.split("/").pop())], + }; + yield* fs.writeFileString(path.join(contentDir, "meta.json"), yield* encodeJson(metaContent)); + + yield* Console.log(`\nGenerated ${pages.length} command page(s)`); + }); + + /** + * Copies `@supabase/config`'s post-processed `dist/*.json` schema artifact to its docs-site + * public path rather than re-rendering it here, since re-rendering would bypass + * `json-schema-postprocess.ts` and produce a mismatched `$id`. Requires + * `@supabase/config#build` to have already run (wired via `turbo.json`). + */ + const copyConfigSchemaAsset = Effect.fnUntraced(function* ( + schemaUrl: string, + distFileName: string, + ) { + const schemaPathname = new URL(schemaUrl).pathname.replace(/^\/docs/, ""); + const destPath = path.join(defaultDocsPublicDir, schemaPathname); + const sourcePath = path.join(configPackageDistDir, distFileName); + + yield* fs.makeDirectory(path.dirname(destPath), { recursive: true }); + yield* fs.copyFile(sourcePath, destPath); + + yield* Console.log( + `Generated: ${path.relative(path.resolve(import.meta.dir, "../../.."), destPath)}`, + ); + }); + + /** Writes the `supabase.lock` v1 JSON Schema, generated from the stack's lock schema, to its docs-site public path. */ + const writeLockSchemaAsset = Effect.fnUntraced(function* () { + const { schema, definitions } = Schema.toJsonSchemaDocument(Lock); + const document = { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: LOCK_SCHEMA_URL, + title: "supabase.lock", + description: "The service versions that supabase start pins in supabase/supabase.lock.", + ...schema, + $defs: definitions, + }; + const schemaPathname = new URL(LOCK_SCHEMA_URL).pathname.replace(/^\/docs/, ""); + const destPath = path.join(defaultDocsPublicDir, schemaPathname); + + yield* fs.makeDirectory(path.dirname(destPath), { recursive: true }); + yield* fs.writeFileString(destPath, `${yield* encodeJson(document)}\n`); + + yield* Console.log( + `Generated: ${path.relative(path.resolve(import.meta.dir, "../../.."), destPath)}`, + ); + }); + + yield* generateCommandDocs(); + yield* writeLockSchemaAsset(); + yield* copyConfigSchemaAsset(CLI_CONFIG_SCHEMA_URL, "schema.json"); + yield* copyConfigSchemaAsset(PROJECT_CONFIG_SCHEMA_URL, "project-schema.json"); +}); + +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + return error.cause !== undefined ? error.cause : new Error(error.message); } -generateCommandDocs(); -writeLockSchemaAsset(); -copyConfigSchemaAsset(CLI_CONFIG_SCHEMA_URL, "schema.json"); -copyConfigSchemaAsset(PROJECT_CONFIG_SCHEMA_URL, "project-schema.json"); +await Effect.runPromise( + main.pipe( + Effect.catch((error) => Effect.die(thrownValue(error))), + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + ), +); diff --git a/apps/cli/scripts/propose-release-notes.ts b/apps/cli/scripts/propose-release-notes.ts index c1ab14505e..225090a74a 100644 --- a/apps/cli/scripts/propose-release-notes.ts +++ b/apps/cli/scripts/propose-release-notes.ts @@ -7,14 +7,24 @@ // Usage: bun apps/cli/scripts/propose-release-notes.ts --tag v2.101.0 [--dry-run] [--apply] // [--render-only] [--model ] import { query, type Options } from "@anthropic-ai/claude-agent-sdk"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { existsSync } from "node:fs"; -import path from "node:path"; +import { Console, Data, Effect, FileSystem, Layer, Path, Stream } from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; import aiModels from "../../../.github/ai-models.json"; +class ShellCommandFailed extends Data.TaggedError("ShellCommandFailed")<{ + readonly cause: unknown; +}> {} + +class AgentQueryFailed extends Data.TaggedError("AgentQueryFailed")<{ readonly cause: unknown }> {} + +const shell = (command: () => Promise) => + Effect.tryPromise({ try: command, catch: (cause) => new ShellCommandFailed({ cause }) }); + +const utf8 = new TextDecoder("utf-8", { ignoreBOM: true }); + const { values } = parseArgs({ options: { tag: { type: "string" }, @@ -26,129 +36,147 @@ const { values } = parseArgs({ strict: true, }); -const tag = values.tag; -if (!tag) { - console.error("--tag is required (e.g. --tag v2.101.0)"); - process.exit(2); -} -const version = tag.replace(/^v/, ""); -const apply = values.apply === true && values["dry-run"] !== true; - -const repoRoot = (await $`git rev-parse --show-toplevel`.text()).trim(); -const promptPath = path.join(repoRoot, "tools/release/release-notes-prompt.md"); -const backfillScript = path.join(repoRoot, "apps/cli/scripts/backfill-release-notes.ts"); -const notesDir = path.join(repoRoot, "release-notes"); -const notesPath = path.join(notesDir, `v${version}.md`); - -console.error(`==> Re-deriving raw semantic-release notes for ${tag}`); -const rawNotes = (await $`bun ${backfillScript} --tag ${tag}`.cwd(repoRoot).text()).trim(); -if (!rawNotes) { - console.error(`backfill-release-notes produced no output for ${tag}`); - process.exit(1); -} +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; -const promptTemplate = await readFile(promptPath, "utf8"); -const placeholder = "{{PASTE_SEMANTIC_RELEASE_BLOCK_HERE}}"; -if (!promptTemplate.includes(placeholder)) { - console.error(`Prompt template at ${promptPath} is missing ${placeholder}`); - process.exit(1); -} -const rendered = promptTemplate.replace(placeholder, rawNotes); + const tag = values.tag; + if (!tag) { + yield* Console.error("--tag is required (e.g. --tag v2.101.0)"); + return yield* Effect.sync(() => process.exit(2)); + } + const version = tag.replace(/^v/, ""); + const apply = values.apply === true && values["dry-run"] !== true; + + const repoRoot = (yield* shell(() => $`git rev-parse --show-toplevel`.text())).trim(); + const promptPath = path.join(repoRoot, "tools/release/release-notes-prompt.md"); + const backfillScript = path.join(repoRoot, "apps/cli/scripts/backfill-release-notes.ts"); + const notesDir = path.join(repoRoot, "release-notes"); + const notesPath = path.join(notesDir, `v${version}.md`); + + yield* Console.error(`==> Re-deriving raw semantic-release notes for ${tag}`); + const rawNotes = (yield* shell(() => + $`bun ${backfillScript} --tag ${tag}`.cwd(repoRoot).text(), + )).trim(); + if (!rawNotes) { + yield* Console.error(`backfill-release-notes produced no output for ${tag}`); + return yield* Effect.sync(() => process.exit(1)); + } -if (values["render-only"]) { - process.stdout.write(rendered); - process.exit(0); -} + const promptTemplate = utf8.decode(yield* fs.readFile(promptPath)); + const placeholder = "{{PASTE_SEMANTIC_RELEASE_BLOCK_HERE}}"; + if (!promptTemplate.includes(placeholder)) { + yield* Console.error(`Prompt template at ${promptPath} is missing ${placeholder}`); + return yield* Effect.sync(() => process.exit(1)); + } + const rendered = promptTemplate.replace(placeholder, rawNotes); -console.error(`==> Running Claude Agent SDK (model=${values.model})`); -const options: Options = { - model: values.model, - // WebFetch/WebSearch investigate PR bodies and linked issues; Bash uses `gh` for - // authenticated GitHub queries instead of HTML scraping. Edit/Write are excluded — the - // script owns the final file output. - allowedTools: ["WebFetch", "WebSearch", "Bash"], - // Skips the repo's CLAUDE.md and settings.json — the prompt is self-contained. - settingSources: [], - cwd: repoRoot, - effort: "low", -}; - -let finalText = ""; -let cost = 0; -const stream = query({ prompt: rendered, options }); -for await (const msg of stream) { - if (msg.type === "result") { - if (msg.subtype === "success") { - finalText = msg.result; - cost = msg.total_cost_usd; - } else { - console.error(`Agent failed: ${msg.subtype}`); - if (msg.errors?.length) console.error(msg.errors.join("\n")); - process.exit(1); - } + if (values["render-only"]) { + return yield* Effect.sync(() => { + process.stdout.write(rendered); + process.exit(0); + }); } -} -if (!finalText.trim()) { - console.error("Agent returned no result text"); - process.exit(1); -} + yield* Console.error(`==> Running Claude Agent SDK (model=${values.model})`); + const options: Options = { + model: values.model, + // WebFetch/WebSearch investigate PR bodies and linked issues; Bash uses `gh` for + // authenticated GitHub queries instead of HTML scraping. Edit/Write are excluded — the + // script owns the final file output. + allowedTools: ["WebFetch", "WebSearch", "Bash"], + // Skips the repo's CLAUDE.md and settings.json — the prompt is self-contained. + settingSources: [], + cwd: repoRoot, + effort: "low", + }; + + let finalText = ""; + let cost = 0; + yield* Stream.fromAsyncIterable( + query({ prompt: rendered, options }), + (cause) => new AgentQueryFailed({ cause }), + ).pipe( + Stream.runForEach((msg) => + Effect.gen(function* () { + if (msg.type === "result") { + if (msg.subtype === "success") { + finalText = msg.result; + cost = msg.total_cost_usd; + } else { + yield* Console.error(`Agent failed: ${msg.subtype}`); + if (msg.errors?.length) yield* Console.error(msg.errors.join("\n")); + return yield* Effect.sync(() => process.exit(1)); + } + } + }), + ), + ); + + if (!finalText.trim()) { + yield* Console.error("Agent returned no result text"); + return yield* Effect.sync(() => process.exit(1)); + } -const normalized = finalText.endsWith("\n") ? finalText : `${finalText}\n`; -console.error(`==> Agent finished (cost ~$${cost.toFixed(4)})`); + const normalized = finalText.endsWith("\n") ? finalText : `${finalText}\n`; + yield* Console.error(`==> Agent finished (cost ~$${cost.toFixed(4)})`); -if (!apply) { - process.stdout.write(normalized); - process.exit(0); -} + if (!apply) { + return yield* Effect.sync(() => { + process.stdout.write(normalized); + process.exit(0); + }); + } -await mkdir(notesDir, { recursive: true }); -if (existsSync(notesPath)) { - console.error( - `Refusing to overwrite existing ${path.relative(repoRoot, notesPath)}. ` + - `Delete it or rerun with --dry-run to preview.`, - ); - process.exit(1); -} -await writeFile(notesPath, normalized); -console.error(`==> Wrote ${path.relative(repoRoot, notesPath)}`); - -const branch = `release-notes/v${version}`; -// Branches from origin/develop, not the checked-out ref, so a feature branch that has diverged -// doesn't drag its commits into the PR; the notes file is untracked, so this leaves it in place. -// Targets develop rather than main so an accidental merge can't rewrite main's history. -await $`git fetch --no-tags origin develop`.cwd(repoRoot).nothrow(); -await $`git checkout -B ${branch} origin/develop`.cwd(repoRoot); -await $`git add ${notesPath}`.cwd(repoRoot); -const commitMessage = `docs(release): propose user-facing notes for ${tag}`; -await $`git commit -m ${commitMessage}`.cwd(repoRoot); - -console.error(`==> Pushing ${branch}`); -let pushed = false; -for (let attempt = 0; attempt < 4; attempt++) { - const result = await $`git push -u origin ${branch}`.cwd(repoRoot).nothrow(); - if (result.exitCode === 0) { - pushed = true; - break; + yield* fs.makeDirectory(notesDir, { recursive: true }); + if (yield* fs.exists(notesPath).pipe(Effect.orElseSucceed(() => false))) { + yield* Console.error( + `Refusing to overwrite existing ${path.relative(repoRoot, notesPath)}. ` + + `Delete it or rerun with --dry-run to preview.`, + ); + return yield* Effect.sync(() => process.exit(1)); + } + yield* fs.writeFileString(notesPath, normalized); + yield* Console.error(`==> Wrote ${path.relative(repoRoot, notesPath)}`); + + const branch = `release-notes/v${version}`; + // Branches from origin/develop, not the checked-out ref, so a feature branch that has diverged + // doesn't drag its commits into the PR; the notes file is untracked, so this leaves it in place. + // Targets develop rather than main so an accidental merge can't rewrite main's history. + yield* shell(() => $`git fetch --no-tags origin develop`.cwd(repoRoot).nothrow()); + yield* shell(() => $`git checkout -B ${branch} origin/develop`.cwd(repoRoot)); + yield* shell(() => $`git add ${notesPath}`.cwd(repoRoot)); + const commitMessage = `docs(release): propose user-facing notes for ${tag}`; + yield* shell(() => $`git commit -m ${commitMessage}`.cwd(repoRoot)); + + yield* Console.error(`==> Pushing ${branch}`); + let pushed = false; + for (let attempt = 0; attempt < 4; attempt++) { + const result = yield* shell(() => $`git push -u origin ${branch}`.cwd(repoRoot).nothrow()); + if (result.exitCode === 0) { + pushed = true; + break; + } + const wait = 2 ** (attempt + 1) * 1000; + yield* Console.error(`Push failed (attempt ${attempt + 1}/4); retrying in ${wait / 1000}s`); + yield* Effect.sleep(wait); + } + if (!pushed) { + yield* Console.error("git push failed after 4 attempts"); + return yield* Effect.sync(() => process.exit(1)); } - const wait = 2 ** (attempt + 1) * 1000; - console.error(`Push failed (attempt ${attempt + 1}/4); retrying in ${wait / 1000}s`); - await new Promise((r) => setTimeout(r, wait)); -} -if (!pushed) { - console.error("git push failed after 4 attempts"); - process.exit(1); -} -// Ensures the `do not merge` label exists before attaching it — a reminder for reviewers; the -// approval workflow never actually merges. -const labelName = "do not merge"; -await $`gh label create ${labelName} --color B60205 --description ${"Approve to apply; do not merge."} --force` - .cwd(repoRoot) - .nothrow(); + // Ensures the `do not merge` label exists before attaching it — a reminder for reviewers; the + // approval workflow never actually merges. + const labelName = "do not merge"; + yield* shell(() => + $`gh label create ${labelName} --color B60205 --description ${"Approve to apply; do not merge."} --force` + .cwd(repoRoot) + .nothrow(), + ); -const releaseUrl = `https://github.com/supabase/cli/releases/tag/${tag}`; -const prBody = `Proposed user-facing release notes for \`${tag}\`, generated by \`apps/cli/scripts/propose-release-notes.ts\` against \`tools/release/release-notes-prompt.md\`. + const releaseUrl = `https://github.com/supabase/cli/releases/tag/${tag}`; + const prBody = `Proposed user-facing release notes for \`${tag}\`, generated by \`apps/cli/scripts/propose-release-notes.ts\` against \`tools/release/release-notes-prompt.md\`. ## How to update the notes @@ -175,7 +203,21 @@ Close the PR without approving. The auto-generated semantic-release body for \`$ After this PR is closed, rerun the **Propose release notes** workflow from the Actions tab against \`${tag}\` to get a fresh proposal. `; -await $`gh pr create --title ${`docs(release): notes for ${tag}`} --body ${prBody} --base develop --head ${branch} --label ${labelName}`.cwd( - repoRoot, + yield* shell(() => + $`gh pr create --title ${`docs(release): notes for ${tag}`} --body ${prBody} --base develop --head ${branch} --label ${labelName}`.cwd( + repoRoot, + ), + ); + yield* Console.error(`==> PR opened for ${branch}`); +}); + +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + return error.cause !== undefined ? error.cause : new Error(error.message); +} + +await Effect.runPromise( + main.pipe( + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + Effect.catch((error) => Effect.die(thrownValue(error))), + ), ); -console.error(`==> PR opened for ${branch}`); diff --git a/apps/cli/scripts/publish-docs-spec.e2e.test.ts b/apps/cli/scripts/publish-docs-spec.e2e.test.ts index 819c6b09d4..f13b0a7340 100644 --- a/apps/cli/scripts/publish-docs-spec.e2e.test.ts +++ b/apps/cli/scripts/publish-docs-spec.e2e.test.ts @@ -1,8 +1,8 @@ -import path from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { stringify } from "yaml"; -const cliRoot = path.resolve(import.meta.dirname, ".."); +const cliRoot = fileURLToPath(new URL("..", import.meta.url)); function runScript( args: ReadonlyArray, diff --git a/apps/cli/scripts/publish-docs-spec.ts b/apps/cli/scripts/publish-docs-spec.ts index 310f2be9ce..a02343d4fd 100644 --- a/apps/cli/scripts/publish-docs-spec.ts +++ b/apps/cli/scripts/publish-docs-spec.ts @@ -7,14 +7,42 @@ * No-op (exit 0) when the spec is already published and no PR is missing; a branch already ahead * of base still gets its PR ensured, so a failed prior run is repaired by the next release. */ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { Console, Data, Effect, FileSystem, Layer, Path } from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; import { parse } from "yaml"; +class ShellCommandFailed extends Data.TaggedError("ShellCommandFailed")<{ + readonly cause: unknown; +}> {} + +class StdinReadFailed extends Data.TaggedError("StdinReadFailed")<{ readonly cause: unknown }> {} + +class SpecYamlInvalid extends Data.TaggedError("SpecYamlInvalid")<{ readonly cause: unknown }> {} + +class JsonFailed extends Data.TaggedError("JsonFailed")<{ readonly cause: unknown }> {} + +const shell = (command: () => Promise) => + Effect.tryPromise({ try: command, catch: (cause) => new ShellCommandFailed({ cause }) }); + +/* Plain `JSON` keeps the engine's own error for a cyclic spec value or malformed `gh` output, + which a `Schema` codec would replace with its issue text. */ +const json = (run: () => A) => + Effect.try({ try: run, catch: (cause) => new JsonFailed({ cause }) }); + +function jsonText(value: unknown): string { + return `${JSON.stringify(value)}`; +} + +function ownPullsOf(existing: string, repo: string) { + const openPulls: Array<{ headRepositoryOwner?: { login?: string } }> = JSON.parse(existing); + return openPulls.filter( + (pull) => pull.headRepositoryOwner?.login?.toLowerCase() === repo.split("/")[0]?.toLowerCase(), + ); +} + const { values } = parseArgs({ options: { version: { type: "string" }, @@ -26,130 +54,166 @@ const { values } = parseArgs({ }, }); -const versionArgument = values.version; -if (!versionArgument) { - console.error( - "Usage: bun scripts/generate-docs-spec.ts | bun scripts/publish-docs-spec.ts --version [--repo ] [--spec-path ] [--branch ] [--base ] [--dry-run]", - ); - process.exit(1); -} -const version = versionArgument.startsWith("v") ? versionArgument.slice(1) : versionArgument; - -const repo = values.repo!; -const specPath = values["spec-path"]!; -const branch = values.branch!; -const base = values.base!; -const dryRun = values["dry-run"]!; - -const spec = await Bun.stdin.text(); -if (spec.trim().length === 0) { - console.error("Refusing to publish an empty spec: nothing was piped in on stdin."); - process.exit(1); -} +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; -let parsed: { clispec?: unknown; info?: { version?: unknown }; commands?: unknown }; -try { - parsed = parse(spec); -} catch (error) { - console.error(`Refusing to publish: stdin is not valid YAML (${error}).`); - process.exit(1); -} -if (parsed?.clispec !== "001") { - console.error( - `Refusing to publish: expected clispec "001", got ${JSON.stringify(parsed?.clispec)}.`, - ); - process.exit(1); -} -if (parsed.info?.version !== version) { - console.error( - `Refusing to publish: the spec's version ${JSON.stringify(parsed.info?.version)} does not match --version ${version}.`, - ); - process.exit(1); -} -const commands = parsed.commands; -if (!Array.isArray(commands) || commands.length < 144) { - console.error( - `Refusing to publish: the spec has ${Array.isArray(commands) ? commands.length : 0} commands, expected at least 144.`, - ); - process.exit(1); -} + const versionArgument = values.version; + if (!versionArgument) { + yield* Console.error( + "Usage: bun scripts/generate-docs-spec.ts | bun scripts/publish-docs-spec.ts --version [--repo ] [--spec-path ] [--branch ] [--base ] [--dry-run]", + ); + return yield* Effect.sync(() => process.exit(1)); + } + const version = versionArgument.startsWith("v") ? versionArgument.slice(1) : versionArgument; -if (dryRun) { - console.log(`Would publish ${spec.length} bytes to ${repo}:${specPath} on branch ${branch}`); - process.exit(0); -} + const repo = values.repo!; + const specPath = values["spec-path"]!; + const branch = values.branch!; + const base = values.base!; + const dryRun = values["dry-run"]!; -// `--depth 1` suffices: an existing `cli/ref-doc` branch is fetched at its tip and the new spec -// lands as one commit on top, so commits pushed onto an open PR survive later releases. -const tmpDir = await mkdtemp(path.join(tmpdir(), "supabase-docs-")); -try { - await $`git clone --quiet --depth 1 --branch ${base} https://github.com/${repo}.git ${tmpDir}`; - - const remoteBranch = await $`git -C ${tmpDir} fetch --quiet --depth 1 origin ${branch}` - .nothrow() - .quiet(); - if (remoteBranch.exitCode === 0) { - await $`git -C ${tmpDir} checkout --quiet -B ${branch} FETCH_HEAD`; - } else { - await $`git -C ${tmpDir} checkout --quiet -B ${branch}`; + const spec = yield* Effect.tryPromise({ + try: () => Bun.stdin.text(), + catch: (cause) => new StdinReadFailed({ cause }), + }); + if (spec.trim().length === 0) { + yield* Console.error("Refusing to publish an empty spec: nothing was piped in on stdin."); + return yield* Effect.sync(() => process.exit(1)); } - const target = path.join(tmpDir, specPath); - await writeFile(target, spec); - const prettier = path.resolve(import.meta.dir, "../node_modules/.bin/prettier"); - await $`${prettier} --no-config --single-quote --print-width 100 --log-level warn --write ${target}`; - - const message = "chore: update cli reference doc"; - await $`git -C ${tmpDir} add ${specPath}`; - const unchanged = await $`git -C ${tmpDir} diff --quiet --cached -- ${specPath}` - .nothrow() - .quiet(); - if (unchanged.exitCode === 0) { - console.log( - remoteBranch.exitCode === 0 - ? `${specPath} is already up to date on ${branch}` - : `${specPath} is already up to date in ${repo}`, + const parsed: { clispec?: unknown; info?: { version?: unknown }; commands?: unknown } = + yield* Effect.try({ + try: () => parse(spec), + catch: (cause) => new SpecYamlInvalid({ cause }), + }).pipe( + Effect.catch((error) => + Console.error(`Refusing to publish: stdin is not valid YAML (${error.cause}).`).pipe( + Effect.andThen(Effect.sync(() => process.exit(1))), + ), + ), + ); + if (parsed?.clispec !== "001") { + yield* Console.error( + `Refusing to publish: expected clispec "001", got ${yield* json(() => jsonText(parsed?.clispec))}.`, ); - } else { - await $`git -C ${tmpDir} -c ${"user.name=github-actions[bot]"} -c ${"user.email=41898282+github-actions[bot]@users.noreply.github.com"} commit --quiet -m ${message}`; - await $`git -C ${tmpDir} push --quiet origin ${branch}`; - console.log(`Pushed ${branch} to ${repo}`); + return yield* Effect.sync(() => process.exit(1)); + } + if (parsed.info?.version !== version) { + yield* Console.error( + `Refusing to publish: the spec's version ${yield* json(() => jsonText(parsed.info?.version))} does not match --version ${version}.`, + ); + return yield* Effect.sync(() => process.exit(1)); + } + const commands = parsed.commands; + if (!Array.isArray(commands) || commands.length < 144) { + yield* Console.error( + `Refusing to publish: the spec has ${Array.isArray(commands) ? commands.length : 0} commands, expected at least 144.`, + ); + return yield* Effect.sync(() => process.exit(1)); } - const unpublished = await $`git -C ${tmpDir} diff --quiet origin/${base} HEAD -- ${specPath}` - .nothrow() - .quiet(); - if (unpublished.exitCode === 1) { - const existing = - await $`gh pr list --repo ${repo} --head ${branch} --base ${base} --state open --json number,headRepositoryOwner` - .cwd(tmpDir) - .text(); - const openPulls: Array<{ headRepositoryOwner?: { login?: string } }> = JSON.parse(existing); - const ownPulls = openPulls.filter( - (pull) => - pull.headRepositoryOwner?.login?.toLowerCase() === repo.split("/")[0]?.toLowerCase(), + if (dryRun) { + yield* Console.log( + `Would publish ${spec.length} bytes to ${repo}:${specPath} on branch ${branch}`, ); - if (ownPulls.length > 0) { - console.log(`Reusing the open PR for ${branch}`); - } else { - const body = [ - "Updates the CLI reference from the supabase/cli release workflow.", - "", - "Generated by `scripts/generate-docs-spec.ts` in supabase/cli — edit the", - "command tree or the content under `apps/cli/docs/` there rather than this", - "file.", - "", - "New commands also need an entry in `apps/docs/spec/common-cli-sections.json` —", - "without one a command's page is silently dropped from the docs site.", - "Sections fixes can be committed directly onto this branch — later releases", - "add commits on top instead of rewriting it.", - ].join("\n"); - await $`gh pr create --repo ${repo} --title ${message} --body ${body} --base ${base} --head ${branch}`.cwd( - tmpDir, - ); - console.log(`Opened a pull request against ${repo}`); - } + return yield* Effect.sync(() => process.exit(0)); } -} finally { - await rm(tmpDir, { recursive: true, force: true }); -} + + // `--depth 1` suffices: an existing `cli/ref-doc` branch is fetched at its tip and the new spec + // lands as one commit on top, so commits pushed onto an open PR survive later releases. + const publishSpec = (tmpDir: string) => + Effect.gen(function* () { + yield* shell( + () => + $`git clone --quiet --depth 1 --branch ${base} https://github.com/${repo}.git ${tmpDir}`, + ); + + const remoteBranch = yield* shell(() => + $`git -C ${tmpDir} fetch --quiet --depth 1 origin ${branch}`.nothrow().quiet(), + ); + if (remoteBranch.exitCode === 0) { + yield* shell(() => $`git -C ${tmpDir} checkout --quiet -B ${branch} FETCH_HEAD`); + } else { + yield* shell(() => $`git -C ${tmpDir} checkout --quiet -B ${branch}`); + } + + const target = path.join(tmpDir, specPath); + yield* fs.writeFileString(target, spec); + const prettier = path.resolve(import.meta.dir, "../node_modules/.bin/prettier"); + yield* shell( + () => + $`${prettier} --no-config --single-quote --print-width 100 --log-level warn --write ${target}`, + ); + + const message = "chore: update cli reference doc"; + yield* shell(() => $`git -C ${tmpDir} add ${specPath}`); + const unchanged = yield* shell(() => + $`git -C ${tmpDir} diff --quiet --cached -- ${specPath}`.nothrow().quiet(), + ); + if (unchanged.exitCode === 0) { + yield* Console.log( + remoteBranch.exitCode === 0 + ? `${specPath} is already up to date on ${branch}` + : `${specPath} is already up to date in ${repo}`, + ); + } else { + yield* shell( + () => + $`git -C ${tmpDir} -c ${"user.name=github-actions[bot]"} -c ${"user.email=41898282+github-actions[bot]@users.noreply.github.com"} commit --quiet -m ${message}`, + ); + yield* shell(() => $`git -C ${tmpDir} push --quiet origin ${branch}`); + yield* Console.log(`Pushed ${branch} to ${repo}`); + } + + const unpublished = yield* shell(() => + $`git -C ${tmpDir} diff --quiet origin/${base} HEAD -- ${specPath}`.nothrow().quiet(), + ); + if (unpublished.exitCode === 1) { + const existing = yield* shell(() => + $`gh pr list --repo ${repo} --head ${branch} --base ${base} --state open --json number,headRepositoryOwner` + .cwd(tmpDir) + .text(), + ); + const ownPulls = yield* json(() => ownPullsOf(existing, repo)); + if (ownPulls.length > 0) { + yield* Console.log(`Reusing the open PR for ${branch}`); + } else { + const body = [ + "Updates the CLI reference from the supabase/cli release workflow.", + "", + "Generated by `scripts/generate-docs-spec.ts` in supabase/cli — edit the", + "command tree or the content under `apps/cli/docs/` there rather than this", + "file.", + "", + "New commands also need an entry in `apps/docs/spec/common-cli-sections.json` —", + "without one a command's page is silently dropped from the docs site.", + "Sections fixes can be committed directly onto this branch — later releases", + "add commits on top instead of rewriting it.", + ].join("\n"); + yield* shell(() => + $`gh pr create --repo ${repo} --title ${message} --body ${body} --base ${base} --head ${branch}`.cwd( + tmpDir, + ), + ); + yield* Console.log(`Opened a pull request against ${repo}`); + } + } + }); + /* A failed removal replaces the run's outcome. */ + yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const tmpDir = yield* fs.makeTempDirectory({ prefix: "supabase-docs-" }); + const outcome = yield* Effect.exit(restore(publishSpec(tmpDir))); + yield* fs.remove(tmpDir, { recursive: true, force: true }); + return yield* outcome; + }), + ); +}); + +await Effect.runPromise( + main.pipe( + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + Effect.catch((error) => Effect.die(error.cause ?? error)), + ), +); diff --git a/apps/cli/scripts/publish.ts b/apps/cli/scripts/publish.ts index 226a1f6978..a40689739e 100644 --- a/apps/cli/scripts/publish.ts +++ b/apps/cli/scripts/publish.ts @@ -1,12 +1,11 @@ import { $ } from "bun"; -import { copyFile } from "node:fs/promises"; -import path from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { Console, Data, Effect, Fiber, FileSystem, Layer, Path, Predicate } from "effect"; +import { FetchHttpClient, HttpClient } from "effect/http"; import { isPublishableNpmTag, PUBLISHABLE_NPM_TAGS_HINT } from "./release-channels.ts"; -const root = path.resolve(import.meta.dir, "../../.."); - const PLATFORM_PACKAGES = [ "cli-darwin-arm64", "cli-darwin-x64", @@ -27,133 +26,195 @@ const { values } = parseArgs({ const dryRun = values["dry-run"]; const tag = values.tag; -if (!isPublishableNpmTag(tag)) { - console.error( - `Invalid --tag value: ${String(tag)}. Expected one of: ${PUBLISHABLE_NPM_TAGS_HINT}.`, - ); - process.exit(1); -} - -const cliDir = path.join(root, "apps/cli"); -const cliPkgJson = await Bun.file(path.join(cliDir, "package.json")).json(); -const umbrellaName: string = cliPkgJson.name; -const umbrellaVersion: string = cliPkgJson.version; const dryRunFlag = dryRun ? ["--dry-run"] : []; const tagFlag = ["--tag", tag]; const provenanceFlag = ["--provenance"]; const noGitChecksFlag = ["--no-git-checks"]; -// Uses `npm config get registry` instead of hard-coding registry.npmjs.org, so the probe and -// publish target agree — needed for the local Verdaccio harness (`pnpm local-registry`). -const registryUrl = (await $`npm config get registry`.quiet().text()).trim().replace(/\/$/, ""); +class PublishStepFailed extends Data.TaggedError("PublishStepFailed")<{ + readonly cause: unknown; +}> {} + +class RegistryProbeFailed extends Data.TaggedError("RegistryProbeFailed")<{ + readonly message: string; +}> {} + +const step = (run: () => PromiseLike) => + Effect.tryPromise({ try: run, catch: (cause) => new PublishStepFailed({ cause }) }); // Used as both a pre-flight skip check and post-failure reconciliation, since a publish can // fail with E403 when the registry CDN cache lags behind an already-succeeded publish. -async function isAlreadyPublished(name: string, version: string): Promise { +const isAlreadyPublished = Effect.fnUntraced(function* ( + registryUrl: string, + name: string, + version: string, +) { const encodedName = name.replace("/", "%2F"); - const res = await fetch(`${registryUrl}/${encodedName}/${version}`, { method: "GET" }); + const res = yield* HttpClient.get(`${registryUrl}/${encodedName}/${version}`); if (res.status === 200) return true; if (res.status === 404) return false; - throw new Error(`npm registry probe for ${name}@${version} returned HTTP ${res.status}`); -} + return yield* new RegistryProbeFailed({ + message: `npm registry probe for ${name}@${version} returned HTTP ${res.status}`, + }); +}); type PublishResult = "published" | "skipped"; // Skips a version already on the registry — checked before publishing and again after a // publish-time conflict — and lets any other failure propagate. -async function publishPackage(opts: { +const publishPackage = Effect.fnUntraced(function* (opts: { + registryUrl: string; name: string; version: string; cwd: string; extraFlags?: string[]; -}): Promise { - const { name, version, cwd, extraFlags = [] } = opts; +}) { + const { registryUrl, name, version, cwd, extraFlags = [] } = opts; const label = `${name}@${version}`; - if (await isAlreadyPublished(name, version)) { - console.log(` [skip] ${label} already published.`); - return "skipped"; + if (yield* isAlreadyPublished(registryUrl, name, version)) { + yield* Console.log(` [skip] ${label} already published.`); + return "skipped" satisfies PublishResult; } - console.log(` Publishing ${label}...`); - try { - await $`pnpm publish ${extraFlags} ${provenanceFlag} ${tagFlag} ${noGitChecksFlag} ${dryRunFlag}`.cwd( + yield* Console.log(` Publishing ${label}...`); + return yield* step(() => + $`pnpm publish ${extraFlags} ${provenanceFlag} ${tagFlag} ${noGitChecksFlag} ${dryRunFlag}`.cwd( cwd, + ), + ).pipe( + Effect.andThen(Console.log(` ${label} published.`)), + Effect.as("published"), + Effect.catchTag("PublishStepFailed", (error) => + Effect.gen(function* () { + if (yield* isAlreadyPublished(registryUrl, name, version)) { + yield* Console.log( + ` [skip] ${label} reported a conflict but is now present on the registry; treating as success.`, + ); + return "skipped" satisfies PublishResult; + } + return yield* error; + }), + ), + ); +}); + +const main = Effect.gen(function* () { + if (!isPublishableNpmTag(tag)) { + yield* Console.error( + `Invalid --tag value: ${String(tag)}. Expected one of: ${PUBLISHABLE_NPM_TAGS_HINT}.`, ); - console.log(` ${label} published.`); - return "published"; - } catch (error) { - if (await isAlreadyPublished(name, version)) { - console.log( - ` [skip] ${label} reported a conflict but is now present on the registry; treating as success.`, - ); - return "skipped"; - } - throw error; + return yield* Effect.sync(() => process.exit(1)); } -} -console.log( - dryRun - ? `Publishing to npm with tag "${tag}" (dry run)...\n` - : `Publishing to npm with tag "${tag}"...\n`, -); + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = path.resolve(import.meta.dir, "../../.."); + const cliDir = path.join(root, "apps/cli"); + const cliPkgJson = yield* step(() => Bun.file(path.join(cliDir, "package.json")).json()); + const umbrellaName: string = cliPkgJson.name; + const umbrellaVersion: string = cliPkgJson.version; + + // Uses `npm config get registry` instead of hard-coding registry.npmjs.org, so the probe and + // publish target agree — needed for the local Verdaccio harness (`pnpm local-registry`). + const registryUrl = (yield* step(() => $`npm config get registry`.quiet().text())) + .trim() + .replace(/\/$/, ""); + + yield* Console.log( + dryRun + ? `Publishing to npm with tag "${tag}" (dry run)...\n` + : `Publishing to npm with tag "${tag}"...\n`, + ); -// Every platform package must already be at the umbrella version; `sync-versions.ts` runs -// before this in the release workflow, so a mismatch means the script ran out of order. -for (const pkg of PLATFORM_PACKAGES) { - const pkgJson = await Bun.file(path.join(root, "packages", pkg, "package.json")).json(); - if (pkgJson.version !== umbrellaVersion) { - console.error( - `Version mismatch: @supabase/${pkg} is ${pkgJson.version}, expected ${umbrellaVersion}. Run sync-versions.ts first.`, + // Every platform package must already be at the umbrella version; `sync-versions.ts` runs + // before this in the release workflow, so a mismatch means the script ran out of order. + for (const pkg of PLATFORM_PACKAGES) { + const pkgJson = yield* step(() => + Bun.file(path.join(root, "packages", pkg, "package.json")).json(), ); - process.exit(1); + if (pkgJson.version !== umbrellaVersion) { + yield* Console.error( + `Version mismatch: @supabase/${pkg} is ${pkgJson.version}, expected ${umbrellaVersion}. Run sync-versions.ts first.`, + ); + return yield* Effect.sync(() => process.exit(1)); + } } -} - -console.log("Publishing platform packages..."); -const platformResults = await Promise.all( - PLATFORM_PACKAGES.map((pkg) => - publishPackage({ - name: `@supabase/${pkg}`, - version: umbrellaVersion, - cwd: path.join(root, "packages", pkg), - extraFlags: ["--access", "public"], - }), - ), -); -console.log("\nBuilding umbrella package shim..."); -await $`pnpm build:shim`.cwd(cliDir); + yield* Console.log("Publishing platform packages..."); + /* Detached so a failed job fails the run without cancelling its siblings; callers observe + completion with Fiber.joinAll. */ + const platformResults = yield* Fiber.joinAll( + yield* Effect.forEach(PLATFORM_PACKAGES, (pkg) => + publishPackage({ + registryUrl, + name: `@supabase/${pkg}`, + version: umbrellaVersion, + cwd: path.join(root, "packages", pkg), + extraFlags: ["--access", "public"], + }).pipe(Effect.forkDetach({ startImmediately: true })), + ), + ); -// npm renders the README from the package directory; apps/cli/README.md documents the source -// layout for contributors, not what npm users should see, so copy the root README over it. -console.log("\nStaging root README for umbrella package..."); -await copyFile(path.join(root, "README.md"), path.join(cliDir, "README.md")); + yield* Console.log("\nBuilding umbrella package shim..."); + yield* step(() => $`pnpm build:shim`.cwd(cliDir)); + + // npm renders the README from the package directory; apps/cli/README.md documents the source + // layout for contributors, not what npm users should see, so copy the root README over it. + yield* Console.log("\nStaging root README for umbrella package..."); + yield* fs.copyFile(path.join(root, "README.md"), path.join(cliDir, "README.md")); + + yield* Console.log(`Publishing umbrella package ${umbrellaName}...`); + const umbrellaResult = yield* publishPackage({ + registryUrl, + name: umbrellaName, + version: umbrellaVersion, + cwd: cliDir, + }); + + const results = [...platformResults, umbrellaResult]; + const publishedCount = results.filter((r) => r === "published").length; + const skippedCount = results.filter((r) => r === "skipped").length; + + yield* Console.log(`\nPublished: ${publishedCount}, Skipped: ${skippedCount}.`); + + // All-skipped is ambiguous: it may mean a downstream-only retry (bytes already on npm) or that + // today's commits silently didn't ship a new version. Warn loudly so a human can decide whether + // to re-cut via workflow_dispatch. + if (publishedCount === 0) { + yield* Console.warn( + `\n[warn] No packages were published — every package was already on the registry at ${umbrellaVersion}.\n` + + ` If today's commits were expected to ship, the version did not advance.\n` + + ` Re-cut as a fresh version via the Release workflow (workflow_dispatch).`, + ); + } -console.log(`Publishing umbrella package ${umbrellaName}...`); -const umbrellaResult = await publishPackage({ - name: umbrellaName, - version: umbrellaVersion, - cwd: cliDir, + yield* Console.log("\nAll packages published successfully."); }); -const results = [...platformResults, umbrellaResult]; -const publishedCount = results.filter((r) => r === "published").length; -const skippedCount = results.filter((r) => r === "skipped").length; - -console.log(`\nPublished: ${publishedCount}, Skipped: ${skippedCount}.`); - -// All-skipped is ambiguous: it may mean a downstream-only retry (bytes already on npm) or that -// today's commits silently didn't ship a new version. Warn loudly so a human can decide whether -// to re-cut via workflow_dispatch. -if (publishedCount === 0) { - console.warn( - `\n[warn] No packages were published — every package was already on the registry at ${umbrellaVersion}.\n` + - ` If today's commits were expected to ship, the version did not advance.\n` + - ` Re-cut as a fresh version via the Release workflow (workflow_dispatch).`, - ); +/* A registry URL that cannot be parsed fails as `fetch` reports an invalid URL. */ +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + if ( + Predicate.isTagged(error, "HttpClientError") && + Predicate.hasProperty(error, "reason") && + Predicate.isTagged(error.reason, "InvalidUrlError") + ) { + return Object.assign(new TypeError("fetch() URL is invalid"), { code: "ERR_INVALID_URL" }); + } + return error.cause !== undefined ? error.cause : new Error(error.message); } -console.log("\nAll packages published successfully."); +await Effect.runPromise( + main.pipe( + Effect.catch((error) => Effect.die(thrownValue(error))), + Effect.provide( + Layer.mergeAll( + BunFileSystem.layer, + BunPath.layer, + FetchHttpClient.layer, + Layer.succeed(HttpClient.TracerPropagationEnabled, false), + ), + ), + ), +); diff --git a/apps/cli/scripts/refresh-e2e-timings.ts b/apps/cli/scripts/refresh-e2e-timings.ts index 1fbf485426..366a50f17b 100644 --- a/apps/cli/scripts/refresh-e2e-timings.ts +++ b/apps/cli/scripts/refresh-e2e-timings.ts @@ -1,12 +1,12 @@ // Rewrites apps/cli/tests/e2e-timings.json from the Vitest results caches that the test-e2e // workflow job uploads as `e2e-timings-` artifacts. -import { spawnSync } from "node:child_process"; -import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { constants } from "node:os"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; +import { BunServices } from "@effect/platform-bun"; +import { Console, Data, Effect, FileSystem, Path, Predicate, Schema } from "effect"; +import { ChildProcess, ChildProcessSpawner } from "effect/process"; import { version as installedVitestVersion } from "vitest/node"; export interface VitestFileResult { @@ -96,75 +96,278 @@ function isResultsCache(value: unknown): value is VitestResultsCache { ); } -function readResultsCaches(dir: string): VitestResultsCache[] { - return readdirSync(dir, { recursive: true, withFileTypes: true }) - .filter((entry) => entry.isFile() && entry.name === "results.json") - .map((entry) => { - const file = join(entry.parentPath, entry.name); - const parsed: unknown = JSON.parse(readFileSync(file, "utf8")); - if (!isResultsCache(parsed)) { - throw new Error(`${file} is not a Vitest results cache`); +class RefreshTimingsError extends Data.TaggedError("RefreshTimingsError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +const platformMessage = (error: { readonly message: string; readonly cause?: unknown }) => + error.cause instanceof Error ? error.cause.message : error.message; + +const toRefreshTimingsError = (error: { readonly message: string; readonly cause?: unknown }) => + new RefreshTimingsError({ message: platformMessage(error), cause: error }); + +const dieWithHostError = (error: { readonly cause?: unknown }) => Effect.die(error.cause ?? error); + +const encodeTimingsJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 2 })); + +const receivedSignal = (error: { readonly message: string; readonly cause?: unknown }) => + /^Process interrupted due to receipt of signal: '(.+)'$/.exec(platformMessage(error))?.[1]; + +const hostCode = (error: { readonly cause?: unknown }) => + Predicate.hasProperty(error.cause, "code") ? error.cause.code : undefined; + +/** + * Lists the regular files under `dir` breadth first, in directory order, following symlinked + * directories but never counting a symlink as a file. + * Only `fs.readLink`'s "not a symlink" answer (`EINVAL`) marks a plain entry; a link, or one + * `readLink` is denied on (a mode-000 link on macOS), is a symlink. A directory that cannot be + * listed fails the whole listing, reported against `dir`. + */ +const listRegularFiles = (dir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const files: string[] = []; + const directories = [{ path: dir, viaSymlink: false }]; + for (let next = directories.shift(); next !== undefined; next = directories.shift()) { + const current = next; + const names = yield* fs.readDirectory(current.path).pipe( + Effect.catch((error) => { + const code = hostCode(error); + return current.viaSymlink && (code === "ENOTDIR" || code === "ENOENT") + ? Effect.succeed([]) + : Effect.fail( + new RefreshTimingsError({ + message: platformMessage(error).replace(`'${current.path}'`, `'${dir}'`), + cause: error, + }), + ); + }), + ); + for (const name of names) { + const entry = path.join(current.path, name); + const isSymlink = yield* fs.readLink(entry).pipe( + Effect.as(true), + Effect.catchTag("PlatformError", (error) => Effect.succeed(hostCode(error) !== "EINVAL")), + ); + if (isSymlink) { + directories.push({ path: entry, viaSymlink: true }); + continue; + } + const type = yield* fs.stat(entry).pipe( + Effect.map((info) => info.type), + Effect.orElseSucceed(() => undefined), + ); + if (type === "Directory") { + directories.push({ path: entry, viaSymlink: false }); + } else if (type === "File") { + files.push(entry); + } } - return parsed; - }); + } + return files; + }); + +/* Keeps a leading byte order mark, which the default decoder drops, so the JSON parse sees it. */ +const utf8 = new TextDecoder("utf-8", { ignoreBOM: true }); + +/* A plain parse, so malformed JSON reports the parser's own message. */ +function parseJson(text: string): unknown { + return JSON.parse(text); } +const readResultsCaches = (dir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const caches: VitestResultsCache[] = []; + for (const file of yield* listRegularFiles(dir)) { + if (path.basename(file) !== "results.json") { + continue; + } + const text = utf8.decode(yield* fs.readFile(file)); + const parsed = yield* Effect.try({ + try: () => parseJson(text), + catch: (cause) => + new RefreshTimingsError({ + message: cause instanceof Error ? cause.message : String(cause), + cause, + }), + }); + if (!isResultsCache(parsed)) { + return yield* new RefreshTimingsError({ message: `${file} is not a Vitest results cache` }); + } + caches.push(parsed); + } + return caches; + }).pipe(Effect.catchTag("PlatformError", (error) => Effect.fail(toRefreshTimingsError(error)))); + const usage = `Usage: bun apps/cli/scripts/refresh-e2e-timings.ts --run [--repo supabase/cli] Downloads the e2e-timings-* artifacts of a test workflow run and rewrites apps/cli/tests/e2e-timings.json from them.`; +const refresh = (run: string, repo: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const downloadDir = yield* Effect.acquireRelease( + fs.makeTempDirectory({ prefix: "e2e-timings-" }).pipe(Effect.catch(dieWithHostError)), + (dir) => + fs.remove(dir, { recursive: true, force: true }).pipe(Effect.catch(dieWithHostError)), + ); + const program = Effect.gen(function* () { + const exitCode = yield* Effect.scoped( + Effect.gen(function* () { + const download = yield* spawner + .spawn( + ChildProcess.make( + "gh", + [ + "run", + "download", + run, + "--repo", + repo, + "--pattern", + "e2e-timings-*", + "--dir", + downloadDir, + ], + { detached: false, stdin: "inherit", stdout: "inherit", stderr: "inherit" }, + ), + ) + .pipe( + Effect.mapError( + (error) => + new RefreshTimingsError({ + message: `could not run gh: ${platformMessage(error)}`, + cause: error, + }), + ), + Effect.catchDefect((defect) => + Effect.fail( + new RefreshTimingsError({ + message: `could not run gh: ${defect instanceof Error ? defect.message : String(defect)}`, + cause: defect, + }), + ), + ), + ); + return yield* download.exitCode.pipe( + Effect.mapError((error) => { + const signal = receivedSignal(error); + return new RefreshTimingsError({ + message: + signal === undefined + ? `gh run download failed: ${platformMessage(error)}` + : `gh run download exited with signal ${signal}`, + cause: error, + }); + }), + ); + }), + ); + if (exitCode !== 0) { + return yield* new RefreshTimingsError({ + message: `gh run download exited with ${exitCode}`, + }); + } + + const caches = yield* readResultsCaches(downloadDir); + if (caches.length === 0) { + return yield* new RefreshTimingsError({ + message: `no results.json found in the e2e-timings-* artifacts of run ${run}`, + }); + } + const timings = yield* Console.consoleWith((out) => + Effect.try({ + try: () => + mergeTimings(caches, { + vitestVersion: installedVitestVersion, + warn: (message) => out.error(message), + }), + catch: (cause) => + new RefreshTimingsError({ + message: cause instanceof Error ? cause.message : String(cause), + cause, + }), + }), + ); + const target = fileURLToPath(new URL("../tests/e2e-timings.json", import.meta.url)); + const json = yield* encodeTimingsJson({ version: 1, files: timings }); + yield* fs.writeFileString(target, `${json}\n`).pipe(Effect.mapError(toRefreshTimingsError)); + yield* Console.error(`wrote ${Object.keys(timings).length} timings to ${target}`); + }); + yield* program.pipe( + Effect.catch((error) => + Console.error(`error: ${error.message}`).pipe( + Effect.andThen( + Effect.sync(() => { + process.exitCode = 1; + }), + ), + ), + ), + ); + }).pipe(Effect.scoped); + +const isSignal = (name: string): name is NodeJS.Signals => Object.hasOwn(constants.signals, name); + +/** + * Holds every signal that already has a handler (Vitest's exit hook) until the run, cleanup + * included, has finished, then raises each held signal again, in arrival order. + */ +const holdSignals = (run: Effect.Effect) => + Effect.gen(function* () { + const received: Array = []; + const hold = (signal: NodeJS.Signals) => { + received.push(signal); + }; + const signals = Object.keys(constants.signals) + .filter(isSignal) + .filter((signal) => process.listenerCount(signal) > 0); + const originalListeners = signals.map((signal) => ({ + signal, + listeners: process.rawListeners(signal), + })); + yield* Effect.acquireRelease( + Effect.sync(() => { + for (const { signal, listeners } of originalListeners) { + process.on(signal, hold); + for (const listener of listeners) { + process.off(signal, listener); + } + } + }), + () => + Effect.sync(() => { + for (const { signal, listeners } of originalListeners) { + for (const listener of listeners) { + process.on(signal, listener); + } + process.off(signal, hold); + } + for (const signal of received) { + process.kill(process.pid, signal); + } + }), + ); + return yield* run; + }).pipe(Effect.scoped); + if (import.meta.main) { const { values } = parseArgs({ args: process.argv.slice(2), options: { run: { type: "string" }, repo: { type: "string", default: "supabase/cli" } }, }); if (!values.run) { - console.error(usage); + await Effect.runPromise(Console.error(usage)); process.exit(2); } - const downloadDir = mkdtempSync(join(tmpdir(), "e2e-timings-")); - try { - const download = spawnSync( - "gh", - [ - "run", - "download", - values.run, - "--repo", - values.repo, - "--pattern", - "e2e-timings-*", - "--dir", - downloadDir, - ], - { stdio: "inherit" }, - ); - if (download.error !== undefined) { - throw new Error(`could not run gh: ${download.error.message}`); - } - if (download.status !== 0) { - throw new Error( - `gh run download exited with ${download.status ?? `signal ${download.signal}`}`, - ); - } - - const caches = readResultsCaches(downloadDir); - if (caches.length === 0) { - throw new Error(`no results.json found in the e2e-timings-* artifacts of run ${values.run}`); - } - const timings = mergeTimings(caches, { - vitestVersion: installedVitestVersion, - warn: (message) => console.error(message), - }); - const target = fileURLToPath(new URL("../tests/e2e-timings.json", import.meta.url)); - writeFileSync(target, `${JSON.stringify({ version: 1, files: timings }, null, 2)}\n`); - console.error(`wrote ${Object.keys(timings).length} timings to ${target}`); - } catch (cause) { - console.error(`error: ${cause instanceof Error ? cause.message : String(cause)}`); - process.exitCode = 1; - } finally { - rmSync(downloadDir, { recursive: true, force: true }); - } + await Effect.runPromise( + holdSignals(refresh(values.run, values.repo).pipe(Effect.provide(BunServices.layer))), + ); } diff --git a/apps/cli/scripts/render-service-dockerfile.rules.unit.test.ts b/apps/cli/scripts/render-service-dockerfile.rules.unit.test.ts index 7a3381fd86..3fb8e7bae2 100644 --- a/apps/cli/scripts/render-service-dockerfile.rules.unit.test.ts +++ b/apps/cli/scripts/render-service-dockerfile.rules.unit.test.ts @@ -6,7 +6,7 @@ import { renderDockerfile } from "./render-service-dockerfile.ts"; // one-line-per-alias check is satisfied by default; each test only mutates what it's // exercising. `vi.mock` factories are hoisted above every other top-level statement, so the // fixture pins are built inline here rather than imported from an outer module. -vi.mock("@supabase/stack/internal/artifacts", async (importOriginal) => { +vi.mock("@supabase/stack/internal/artifacts", (importOriginal) => { const nativePin = { archive: "a".repeat(64), manifest: "b".repeat(64) }; const natives = { "darwin-arm64": nativePin, "linux-amd64": nativePin, "linux-arm64": nativePin }; const pin = (sourceService: string, upstreamImage: string, isDefault: boolean) => ({ @@ -21,8 +21,8 @@ vi.mock("@supabase/stack/internal/artifacts", async (importOriginal) => { natives, }, }); - return { - ...(await importOriginal()), + return importOriginal().then((actual) => ({ + ...actual, catalogPins: () => [ pin("postgres", "supabase/postgres:17.0.0", true), pin("postgres", "supabase/postgres:15.0.0", false), @@ -40,7 +40,7 @@ vi.mock("@supabase/stack/internal/artifacts", async (importOriginal) => { pin("storage", "supabase/storage-api:v1.0.0", true), pin("analytics", "supabase/logflare:1.0.0", true), ], - }; + })); }); /** A minimal Dockerfile with every slim-capable alias, plus kong and pg14 (hand-managed). */ diff --git a/apps/cli/scripts/render-service-dockerfile.ts b/apps/cli/scripts/render-service-dockerfile.ts index 79327facf3..997c869d36 100644 --- a/apps/cli/scripts/render-service-dockerfile.ts +++ b/apps/cli/scripts/render-service-dockerfile.ts @@ -16,6 +16,7 @@ import { isOrioledbVersion, type ArtifactPin, } from "@supabase/stack/internal/artifacts"; +import { Console, Data, Effect } from "effect"; // Resolved from this module's own URL, so both the CLI invocation (cwd = repo root) and the // vitest unit test (cwd = apps/cli) read the same files. @@ -160,43 +161,58 @@ export function renderDockerfile(currentDockerfile: string): string { return lines.join("\n"); } -async function main(argv: ReadonlyArray): Promise { +class RenderStepFailed extends Data.TaggedError("RenderStepFailed")<{ readonly cause: unknown }> {} + +const step = (run: () => PromiseLike) => + Effect.tryPromise({ try: run, catch: (cause) => new RenderStepFailed({ cause }) }); + +const main = Effect.fnUntraced(function* (argv: ReadonlyArray) { const check = argv.includes("--check"); - const current = await Bun.file(TS_DOCKERFILE_PATH).text(); - const rendered = renderDockerfile(current); + const current = yield* step(() => Bun.file(TS_DOCKERFILE_PATH).text()); + const rendered = yield* Effect.try({ + try: () => renderDockerfile(current), + catch: (cause) => new RenderStepFailed({ cause }), + }); if (check) { - const go = await Bun.file(GO_DOCKERFILE_PATH).text(); + const go = yield* step(() => Bun.file(GO_DOCKERFILE_PATH).text()); let failed = false; if (current !== rendered) { - console.log( + yield* Console.log( `::error ::${TS_DOCKERFILE_PATH} has drifted from packages/stack/src/Artifacts.ts. Run ` + "`bun apps/cli/scripts/render-service-dockerfile.ts` and commit the result.", ); failed = true; } if (go !== rendered) { - console.log( + yield* Console.log( `::error ::${GO_DOCKERFILE_PATH} is not a byte copy of ${TS_DOCKERFILE_PATH}. Run ` + "`bun apps/cli/scripts/render-service-dockerfile.ts` and commit the result.", ); failed = true; } if (failed) { - process.exit(1); + return yield* Effect.sync(() => process.exit(1)); } - console.log("Both Dockerfiles match the catalog."); + yield* Console.log("Both Dockerfiles match the catalog."); return; } - await Bun.write(TS_DOCKERFILE_PATH, rendered); - await Bun.write(GO_DOCKERFILE_PATH, rendered); - console.log(`Regenerated ${TS_DOCKERFILE_PATH} and ${GO_DOCKERFILE_PATH} from the catalog.`); -} + yield* step(() => Bun.write(TS_DOCKERFILE_PATH, rendered)); + yield* step(() => Bun.write(GO_DOCKERFILE_PATH, rendered)); + yield* Console.log( + `Regenerated ${TS_DOCKERFILE_PATH} and ${GO_DOCKERFILE_PATH} from the catalog.`, + ); +}); if (import.meta.main) { - main(process.argv.slice(2)).catch((error: unknown) => { - console.log(`::error ::${error instanceof Error ? error.message : String(error)}`); - process.exit(1); - }); + await Effect.runPromise( + main(process.argv.slice(2)).pipe( + Effect.catch(({ cause }) => + Console.log(`::error ::${cause instanceof Error ? cause.message : String(cause)}`).pipe( + Effect.andThen(Effect.sync(() => process.exit(1))), + ), + ), + ), + ); } diff --git a/apps/cli/scripts/render-service-dockerfile.unit.test.ts b/apps/cli/scripts/render-service-dockerfile.unit.test.ts index 029a235af8..73f9a5b449 100644 --- a/apps/cli/scripts/render-service-dockerfile.unit.test.ts +++ b/apps/cli/scripts/render-service-dockerfile.unit.test.ts @@ -1,4 +1,3 @@ -import { readFileSync } from "node:fs"; import { describe, expect, test } from "vitest"; import { @@ -7,9 +6,15 @@ import { renderDockerfile, } from "./render-service-dockerfile.ts"; +/* Decoded as UTF-8 with a leading byte order mark kept (`.text()` would drop it). */ +const readDockerfile = (path: string) => + Bun.file(path) + .bytes() + .then((bytes) => new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes)); + // Both against the real, checked-in Dockerfile and the real catalog — no hardcoded version // literal, so a catalog bump never makes this drift-detection test itself go stale. -const currentTsDockerfile = readFileSync(TS_DOCKERFILE_PATH, "utf8"); +const currentTsDockerfile = await readDockerfile(TS_DOCKERFILE_PATH); describe("renderDockerfile against the real catalog and Dockerfile", () => { test("round-trips: rewriting the checked-in Dockerfile reproduces it byte for byte (no drift)", () => { @@ -22,10 +27,10 @@ describe("renderDockerfile against the real catalog and Dockerfile", () => { // The Go tree still `go:embed`s its own copy for a dependency that hasn't been removed yet; // this is the single place that keeps the two copies in sync until apps/cli-go is deleted. - test("the Go tree's embedded Dockerfile is a byte copy of the TS-owned one", () => { - const goDockerfile = readFileSync(GO_DOCKERFILE_PATH, "utf8"); - expect(goDockerfile).toBe(currentTsDockerfile); - }); + test("the Go tree's embedded Dockerfile is a byte copy of the TS-owned one", () => + readDockerfile(GO_DOCKERFILE_PATH).then((goDockerfile) => { + expect(goDockerfile).toBe(currentTsDockerfile); + })); test("detects drift when a generated line is hand-edited", () => { // A hand-edit to a generated tag (bumping gotrue without touching the catalog) must not diff --git a/apps/cli/scripts/sweep-live-projects.integration.test.ts b/apps/cli/scripts/sweep-live-projects.integration.test.ts index faaaac4c2b..5b4a084672 100644 --- a/apps/cli/scripts/sweep-live-projects.integration.test.ts +++ b/apps/cli/scripts/sweep-live-projects.integration.test.ts @@ -1,10 +1,17 @@ -import { afterEach, describe, expect, test } from "vitest"; -import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; - -const script = path.resolve(import.meta.dirname, "sweep-live-projects.sh"); -const directories: string[] = []; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { + Config, + ConfigProvider, + Data, + Effect, + Fiber, + FileSystem, + Layer, + Option, + Path, + PlatformError, +} from "effect"; // Per-sweep hang guard: a spawned run gets this long before the safety net kills // it. A test's own timeout must cover every sweep it runs plus report margin, so @@ -13,9 +20,26 @@ const SWEEP_GUARD_MS = 30_000; const REPORT_MARGIN_MS = 10_000; const sweepBudget = (sweeps: number) => sweeps * SWEEP_GUARD_MS + REPORT_MARGIN_MS; -afterEach(async () => { - await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true }))); -}); +class SweepHarnessFailed extends Data.TaggedError("SweepHarnessFailed")<{ + readonly cause: unknown; +}> {} + +const sweepLayer = Layer.mergeAll( + BunFileSystem.layer, + BunPath.layer, + ConfigProvider.layer(ConfigProvider.fromEnvRecord(process.env, { preserveEmptyStrings: true })), +); + +const provideSweep = ( + sweep: Effect.Effect, +) => + sweep.pipe( + Effect.catchTags({ + PlatformError: (error) => Effect.die(error.cause ?? error), + SweepHarnessFailed: (error) => Effect.die(error.cause), + }), + Effect.provide(sweepLayer), + ); type Scenario = { lists: Array | ((deletes: ReadonlyArray) => unknown); @@ -23,84 +47,106 @@ type Scenario = { deleteStatuses?: Record>; }; -async function runSweep(scenario: Scenario) { - const directory = await mkdtemp(path.join(tmpdir(), "sweep-live-projects-")); - directories.push(directory); +const readText = (stream: ReadableStream) => + Effect.tryPromise({ + try: () => new Response(stream).text(), + catch: (cause) => new SweepHarnessFailed({ cause }), + }); + +const runSweep = Effect.fnUntraced(function* (scenario: Scenario) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const script = path.resolve(import.meta.dirname, "sweep-live-projects.sh"); + const directory = yield* fs.makeTempDirectoryScoped({ prefix: "sweep-live-projects-" }); const fakeSleep = path.join(directory, "sleep"); - await writeFile(fakeSleep, "#!/bin/sh\nexit 0\n"); - await chmod(fakeSleep, 0o755); + yield* fs.writeFileString(fakeSleep, "#!/bin/sh\nexit 0\n"); + yield* fs.chmod(fakeSleep, 0o755); let listIndex = 0; const deletes: string[] = []; - const server = Bun.serve({ - port: 0, - fetch(request) { - const url = new URL(request.url); - const ref = url.pathname.split("/").pop() ?? ""; - if (request.method === "GET" && url.pathname === "/v1/projects") { - const value = - typeof scenario.lists === "function" - ? scenario.lists(deletes) - : scenario.lists[Math.min(listIndex++, scenario.lists.length - 1)]; - return typeof value === "number" - ? new Response("temporary failure", { status: value }) - : Response.json(value); - } - if (request.method === "DELETE" && url.pathname.startsWith("/v1/projects/")) { - deletes.push(ref); - const statuses = scenario.deleteStatuses?.[ref]; - const result: { status: number; body?: unknown } = - statuses === undefined - ? (scenario.deletes[ref] ?? { status: 404 }) - : { - status: - statuses[ - Math.min( - deletes.filter((value) => value === ref).length - 1, - statuses.length - 1, - ) - ] ?? 500, - }; - return new Response(result.body === undefined ? null : JSON.stringify(result.body), { - status: result.status, - headers: { "x-request-id": `delete-${ref}` }, - }); - } - return new Response("not found", { status: 404 }); - }, - }); - try { - const child = Bun.spawn(["bash", script, "e2e-"], { - env: { - ...globalThis.process.env, - PATH: `${directory}:${globalThis.process.env.PATH}`, - SUPABASE_ACCESS_TOKEN: "test-token", - SUPABASE_LIVE_API_URL: `http://127.0.0.1:${server.port}`, + const serve = () => + Bun.serve({ + port: 0, + fetch(request) { + const url = new URL(request.url); + const ref = url.pathname.split("/").pop() ?? ""; + if (request.method === "GET" && url.pathname === "/v1/projects") { + const value = + typeof scenario.lists === "function" + ? scenario.lists(deletes) + : scenario.lists[Math.min(listIndex++, scenario.lists.length - 1)]; + return typeof value === "number" + ? new Response("temporary failure", { status: value }) + : Response.json(value); + } + if (request.method === "DELETE" && url.pathname.startsWith("/v1/projects/")) { + deletes.push(ref); + const statuses = scenario.deleteStatuses?.[ref]; + const result: { status: number; body?: unknown } = + statuses === undefined + ? (scenario.deletes[ref] ?? { status: 404 }) + : { + status: + statuses[ + Math.min( + deletes.filter((value) => value === ref).length - 1, + statuses.length - 1, + ) + ] ?? 500, + }; + return new Response(result.body === undefined ? null : JSON.stringify(result.body), { + status: result.status, + headers: { "x-request-id": `delete-${ref}` }, + }); + } + return new Response("not found", { status: 404 }); }, - stdout: "pipe", - stderr: "pipe", + }); + return yield* Effect.gen(function* () { + const server = yield* Effect.acquireRelease( + Effect.try({ try: serve, catch: (cause) => new SweepHarnessFailed({ cause }) }), + (server) => Effect.promise(() => server.stop(true)), + ); + const inheritedPath = yield* Config.option(Config.String("PATH")); + const child = yield* Effect.try({ + try: () => + Bun.spawn(["bash", script, "e2e-"], { + env: { + ...globalThis.process.env, + PATH: `${directory}:${Option.getOrUndefined(inheritedPath)}`, + SUPABASE_ACCESS_TOKEN: "test-token", + SUPABASE_LIVE_API_URL: `http://127.0.0.1:${server.port}`, + }, + stdout: "pipe", + stderr: "pipe", + }), + catch: (cause) => new SweepHarnessFailed({ cause }), }); let hung = false; - const timeout = setTimeout(() => { - hung = true; - child.kill(); - }, SWEEP_GUARD_MS); - const [exitCode, stdout, stderr] = await Promise.all([ - child.exited, - new Response(child.stdout).text(), - new Response(child.stderr).text(), - ]); - clearTimeout(timeout); + const guard = yield* Effect.sleep(SWEEP_GUARD_MS).pipe( + Effect.andThen( + Effect.sync(() => { + hung = true; + child.kill(); + }), + ), + Effect.forkChild, + ); + const [exitCode, stdout, stderr] = yield* Effect.all( + [Effect.promise(() => child.exited), readText(child.stdout), readText(child.stderr)], + { concurrency: "unbounded" }, + ); + yield* Fiber.interrupt(guard); if (hung) { - throw new Error( - `sweep script hung: killed after ${SWEEP_GUARD_MS}ms without exiting (stdout: ${stdout}, stderr: ${stderr})`, + return yield* Effect.die( + new Error( + `sweep script hung: killed after ${SWEEP_GUARD_MS}ms without exiting (stdout: ${stdout}, stderr: ${stderr})`, + ), ); } return { exitCode, stdout, stderr, deletes }; - } finally { - await server.stop(true); - } -} + }).pipe(Effect.scoped); +}, provideSweep); const active = (ref: string, name = `e2e-${ref}`) => ({ ref, name, status: "ACTIVE" }); @@ -108,87 +154,101 @@ describe.skipIf(process.platform === "win32")( "sweep-live-projects.sh", { timeout: sweepBudget(1) }, () => { - test("accepts refused deletion when a fresh authenticated list shows absence", async () => { - const result = await runSweep({ - lists: [[active("gone")], []], - deletes: { gone: { status: 403, body: { message: "already removed" } } }, - }); - expect(result.exitCode).toBe(0); - expect(result.deletes).toEqual(["gone"]); - expect(`${result.stdout}\n${result.stderr}`).not.toContain("test-token"); - }); - - test("accepts a terminal status and converges after a stale list", async () => { - const result = await runSweep({ - lists: [[active("stale")], [active("stale")], [{ ...active("stale"), status: "REMOVED" }]], - deletes: { stale: { status: 202 } }, - }); - expect(result.exitCode).toBe(0); - }); + it.live("accepts refused deletion when a fresh authenticated list shows absence", () => + Effect.gen(function* () { + const result = yield* runSweep({ + lists: [[active("gone")], []], + deletes: { gone: { status: 403, body: { message: "already removed" } } }, + }); + expect(result.exitCode).toBe(0); + expect(result.deletes).toEqual(["gone"]); + expect(`${result.stdout}\n${result.stderr}`).not.toContain("test-token"); + }), + ); - test( - "retries a transient read failure but rejects malformed evidence", - async () => { - const transient = await runSweep({ - lists: [503, [active("retry")], []], - deletes: { retry: { status: 202 } }, + it.live("accepts a terminal status and converges after a stale list", () => + Effect.gen(function* () { + const result = yield* runSweep({ + lists: [ + [active("stale")], + [active("stale")], + [{ ...active("stale"), status: "REMOVED" }], + ], + deletes: { stale: { status: 202 } }, }); - expect(transient.exitCode).toBe(0); + expect(result.exitCode).toBe(0); + }), + ); - const malformed = await runSweep({ lists: [{ projects: [] }], deletes: {} }); - expect(malformed.exitCode).not.toBe(0); - expect(malformed.deletes).toEqual([]); - }, + it.live( + "retries a transient read failure but rejects malformed evidence", + () => + Effect.gen(function* () { + const transient = yield* runSweep({ + lists: [503, [active("retry")], []], + deletes: { retry: { status: 202 } }, + }); + expect(transient.exitCode).toBe(0); + + const malformed = yield* runSweep({ lists: [{ projects: [] }], deletes: {} }); + expect(malformed.exitCode).not.toBe(0); + expect(malformed.deletes).toEqual([]); + }), sweepBudget(2), ); - test( + it.live( "fails terminal listing errors without retrying or deleting", - async () => { - const forbidden = await runSweep({ lists: [403, []], deletes: {} }); - expect(forbidden.exitCode).not.toBe(0); - expect(forbidden.deletes).toEqual([]); - - const unavailable = await runSweep({ lists: [503, 503, 503], deletes: {} }); - expect(unavailable.exitCode).not.toBe(0); - expect(unavailable.deletes).toEqual([]); - - const malformedReconciliation = await runSweep({ - lists: [[active("bad-evidence")], { projects: [] }], - deletes: { "bad-evidence": { status: 403 } }, - }); - expect(malformedReconciliation.exitCode).not.toBe(0); - }, + () => + Effect.gen(function* () { + const forbidden = yield* runSweep({ lists: [403, []], deletes: {} }); + expect(forbidden.exitCode).not.toBe(0); + expect(forbidden.deletes).toEqual([]); + + const unavailable = yield* runSweep({ lists: [503, 503, 503], deletes: {} }); + expect(unavailable.exitCode).not.toBe(0); + expect(unavailable.deletes).toEqual([]); + + const malformedReconciliation = yield* runSweep({ + lists: [[active("bad-evidence")], { projects: [] }], + deletes: { "bad-evidence": { status: 403 } }, + }); + expect(malformedReconciliation.exitCode).not.toBe(0); + }), sweepBudget(3), ); - test("attempts every owned project and fails if one remains active", async () => { - const result = await runSweep({ - lists: [ - [ - active("stuck"), - active("other"), - { ref: "foreign", name: "unrelated", status: "ACTIVE" }, + it.live("attempts every owned project and fails if one remains active", () => + Effect.gen(function* () { + const result = yield* runSweep({ + lists: [ + [ + active("stuck"), + active("other"), + { ref: "foreign", name: "unrelated", status: "ACTIVE" }, + ], + [active("stuck"), { ref: "foreign", name: "unrelated", status: "ACTIVE" }], ], - [active("stuck"), { ref: "foreign", name: "unrelated", status: "ACTIVE" }], - ], - deletes: { stuck: { status: 403 }, other: { status: 204 } }, - }); - expect(result.exitCode).not.toBe(0); - expect(result.deletes).toEqual(["stuck", "other"]); - }); + deletes: { stuck: { status: 403 }, other: { status: 204 } }, + }); + expect(result.exitCode).not.toBe(0); + expect(result.deletes).toEqual(["stuck", "other"]); + }), + ); - test("retries a transient deletion after fresh evidence still shows the project", async () => { - const result = await runSweep({ - lists: (deletes) => (deletes.length >= 2 ? [] : [active("flaky")]), - deletes: {}, - deleteStatuses: { flaky: [503, 204] }, - }); - expect(result.exitCode).toBe(0); - expect(result.deletes).toEqual(["flaky", "flaky"]); - expect(result.stderr).toContain( - "delete retry completed for project flaky (HTTP 204 request delete-flaky)", - ); - }); + it.live("retries a transient deletion after fresh evidence still shows the project", () => + Effect.gen(function* () { + const result = yield* runSweep({ + lists: (deletes) => (deletes.length >= 2 ? [] : [active("flaky")]), + deletes: {}, + deleteStatuses: { flaky: [503, 204] }, + }); + expect(result.exitCode).toBe(0); + expect(result.deletes).toEqual(["flaky", "flaky"]); + expect(result.stderr).toContain( + "delete retry completed for project flaky (HTTP 204 request delete-flaky)", + ); + }), + ); }, ); diff --git a/apps/cli/scripts/sync-versions.ts b/apps/cli/scripts/sync-versions.ts index 1f4cfe4fb5..e7d01434ac 100644 --- a/apps/cli/scripts/sync-versions.ts +++ b/apps/cli/scripts/sync-versions.ts @@ -1,6 +1,7 @@ import { parseArgs } from "node:util"; -import path from "node:path"; import process from "node:process"; +import { BunPath } from "@effect/platform-bun"; +import { Console, Data, Effect, Path, Schema } from "effect"; const PACKAGE_PATHS = { cli: ["apps", "cli"], @@ -22,22 +23,52 @@ const { values } = parseArgs({ }, }); +class PackageJsonFailed extends Data.TaggedError("PackageJsonFailed")<{ + readonly cause: unknown; +}> {} + +const encodePackageJson = Schema.encodeEffect( + Schema.fromJsonString(Schema.Unknown, { space: "\t" }), +); + const version = values.version; -if (!version) { - console.error("Usage: bun apps/cli/scripts/sync-versions.ts --version "); - process.exit(1); -} -const root = path.resolve(import.meta.dir, "../../.."); +const main = Effect.gen(function* () { + if (!version) { + yield* Console.error("Usage: bun apps/cli/scripts/sync-versions.ts --version "); + process.exit(1); + } + + const path = yield* Path.Path; + const root = path.resolve(import.meta.dir, "../../.."); -for (const pkg of ALL_PACKAGES) { - const pkgJsonPath = path.join(root, ...PACKAGE_PATHS[pkg], "package.json"); - const pkgJson = await Bun.file(pkgJsonPath).json(); + for (const pkg of ALL_PACKAGES) { + const pkgJsonPath = path.join(root, ...PACKAGE_PATHS[pkg], "package.json"); + const pkgJson = yield* Effect.tryPromise({ + try: () => Bun.file(pkgJsonPath).json(), + catch: (cause) => new PackageJsonFailed({ cause }), + }); - pkgJson.version = version; + pkgJson.version = version; + + const contents = `${yield* encodePackageJson(pkgJson)}\n`; + yield* Effect.tryPromise({ + try: () => Bun.write(pkgJsonPath, contents), + catch: (cause) => new PackageJsonFailed({ cause }), + }); + yield* Console.log(`Updated ${pkg} to v${version}`); + } + + yield* Console.log(`\nAll packages synced to v${version}.`); +}); - await Bun.write(pkgJsonPath, `${JSON.stringify(pkgJson, null, "\t")}\n`); - console.log(`Updated ${pkg} to v${version}`); +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + return error.cause !== undefined ? error.cause : new Error(error.message); } -console.log(`\nAll packages synced to v${version}.`); +await Effect.runPromise( + main.pipe( + Effect.catch((error) => Effect.die(thrownValue(error))), + Effect.provide(BunPath.layer), + ), +); diff --git a/apps/cli/scripts/trace-report.ts b/apps/cli/scripts/trace-report.ts index 29cb80c39d..a16430839c 100644 --- a/apps/cli/scripts/trace-report.ts +++ b/apps/cli/scripts/trace-report.ts @@ -27,7 +27,8 @@ const OtlpSpan = Schema.Struct({ name: Schema.String, startTimeUnixNano: Schema.String, endTimeUnixNano: Schema.String, - status: Schema.Struct({ code: Schema.Number }), + /* Any JSON number, including `1e309` (parses to `Infinity`), which `Schema.Finite` rejects. */ + status: Schema.Struct({ code: Schema.declare((u): u is number => typeof u === "number") }), attributes: Schema.optional(KeyValues), events: Schema.optional( Schema.Array(Schema.Struct({ name: Schema.String, attributes: KeyValues })), @@ -44,6 +45,8 @@ const TraceBatch = Schema.fromJsonString( }), ); +const ReportJson = Schema.fromJsonString(Schema.Unknown, { space: 2 }); + export interface ReportSpan { readonly traceId: string; readonly spanId: string; @@ -180,7 +183,7 @@ export const readSpans = Effect.fnUntraced(function* (file: string) { let skippedLines = 0; for (const line of text.split("\n")) { if (line.trim().length === 0) continue; - const decoded = yield* Schema.decodeUnknownEffect(TraceBatch)(line).pipe(Effect.option); + const decoded = yield* Schema.decodeEffect(TraceBatch)(line).pipe(Effect.option); if (decoded._tag === "None") { skippedLines += 1; continue; @@ -302,7 +305,11 @@ const main = Effect.gen(function* () { const header = `trace: ${trace.traceId}${trace.traceCount > 1 ? ` (latest of ${trace.traceCount}; choose one with --trace-id)` : ""}`; yield* Console.log( options.json - ? JSON.stringify({ traceId: trace.traceId, traceCount: trace.traceCount, ...report }, null, 2) + ? yield* Schema.encodeEffect(ReportJson)({ + traceId: trace.traceId, + traceCount: trace.traceCount, + ...report, + }) : `${header}\n${formatReport(report)}`, ); }); diff --git a/apps/cli/scripts/trace-report.unit.test.ts b/apps/cli/scripts/trace-report.unit.test.ts index 66e7bb5140..c2fff6938e 100644 --- a/apps/cli/scripts/trace-report.unit.test.ts +++ b/apps/cli/scripts/trace-report.unit.test.ts @@ -1,6 +1,6 @@ import { BunServices } from "@effect/platform-bun"; import { Effect, FileSystem } from "effect"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "@effect/vitest"; import { analyzeTrace, parseTraceReportArgs, @@ -115,7 +115,7 @@ describe("reportSpanName", () => { }); describe("readSpans", () => { - it("keeps decodable batches and counts lines that do not decode", async () => { + it.effect("keeps decodable batches and counts lines that do not decode", () => { const batch = JSON.stringify({ resourceSpans: [ { @@ -137,19 +137,15 @@ describe("readSpans", () => { ], }); - const result = await Effect.runPromise( - Effect.scoped( - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const file = yield* fs.makeTempFileScoped({ suffix: ".jsonl" }); - yield* fs.writeFileString(file, `${batch}\n{"resourceSpans": [\n`); - return yield* readSpans(file); - }), - ).pipe(Effect.provide(BunServices.layer)), - ); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const file = yield* fs.makeTempFileScoped({ suffix: ".jsonl" }); + yield* fs.writeFileString(file, `${batch}\n{"resourceSpans": [\n`); + const result = yield* readSpans(file); - expect(result.spans.map((span) => span.name)).toEqual(["cli.run"]); - expect(result.skippedLines).toBe(1); + expect(result.spans.map((span) => span.name)).toEqual(["cli.run"]); + expect(result.skippedLines).toBe(1); + }).pipe(Effect.provide(BunServices.layer)); }); }); diff --git a/apps/cli/scripts/update-homebrew.ts b/apps/cli/scripts/update-homebrew.ts index 942cf61a0d..87915f99f6 100644 --- a/apps/cli/scripts/update-homebrew.ts +++ b/apps/cli/scripts/update-homebrew.ts @@ -1,9 +1,8 @@ import { $ } from "bun"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { Console, Data, Effect, FileSystem, Layer, Path } from "effect"; import { homebrewClassName, homebrewConflictsLine } from "./release-channels.ts"; const { values } = parseArgs({ @@ -17,57 +16,69 @@ const { values } = parseArgs({ }, }); -const version = values.version; -if (!version) { - console.error( - "Usage: bun run scripts/update-homebrew.ts --version [--repo ] [--tap ] [--name ] [--local] [--dry-run]", - ); - process.exit(1); -} +class ShellFailed extends Data.TaggedError("ShellFailed")<{ readonly cause: unknown }> {} -const repo = values.repo!; -const tap = values.tap!; -const name = values.name!; -const local = values.local!; -const dryRun = values["dry-run"]!; -const root = path.resolve(import.meta.dir, "../../.."); -const distDir = path.join(root, "dist"); - -// The class and filename vary by channel (e.g. "supabase-beta" -> "SupabaseBeta", "supabase@2" -> -// "SupabaseAT2"), but the installed binary is always `supabase`. -const className = homebrewClassName(name); - -// The Go sidecar is looked up by exact filename next to the running binary, so it must install -// under its original name; `if File.exist?` keeps the formula working when a build ships only -// the CLI binary. -const installBlock = [ - ` bin.install "supabase"`, - ` bin.install "supabase-go" if File.exist?("supabase-go")`, -].join("\n"); - -const conflictsBlock = homebrewConflictsLine(name); -const conflictsSection = conflictsBlock ? `${conflictsBlock}\n\n` : ""; - -const testInvocation = `#{bin}/supabase`; - -const checksums = new Map(); -const checksumsText = await readFile(path.join(distDir, "checksums.txt"), "utf-8"); -for (const line of checksumsText.trim().split("\n")) { - const [hash, file] = line.split(/\s+/) as [string, string]; - checksums.set(file, hash); -} +class FormulaError extends Data.TaggedError("FormulaError")<{ readonly message: string }> {} -function sha(file: string): string { - const hash = checksums.get(file); - if (!hash) throw new Error(`Checksum not found for ${file}`); - return hash; -} +const shell = (command: () => PromiseLike) => + Effect.tryPromise({ try: command, catch: (cause) => new ShellFailed({ cause }) }); + +const main = Effect.gen(function* () { + const version = values.version; + if (!version) { + yield* Console.error( + "Usage: bun run scripts/update-homebrew.ts --version [--repo ] [--tap ] [--name ] [--local] [--dry-run]", + ); + return yield* Effect.sync(() => process.exit(1)); + } + + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const repo = values.repo!; + const tap = values.tap!; + const name = values.name!; + const local = values.local!; + const dryRun = values["dry-run"]!; + const root = path.resolve(import.meta.dir, "../../.."); + const distDir = path.join(root, "dist"); + + // The class and filename vary by channel (e.g. "supabase-beta" -> "SupabaseBeta", "supabase@2" -> + // "SupabaseAT2"), but the installed binary is always `supabase`. + const className = homebrewClassName(name); + + // The Go sidecar is looked up by exact filename next to the running binary, so it must install + // under its original name; `if File.exist?` keeps the formula working when a build ships only + // the CLI binary. + const installBlock = [ + ` bin.install "supabase"`, + ` bin.install "supabase-go" if File.exist?("supabase-go")`, + ].join("\n"); + + const conflictsBlock = homebrewConflictsLine(name); + const conflictsSection = conflictsBlock ? `${conflictsBlock}\n\n` : ""; + + const testInvocation = `#{bin}/supabase`; + + const checksums = new Map(); + const checksumsText = new TextDecoder("utf-8", { ignoreBOM: true }).decode( + yield* fs.readFile(path.join(distDir, "checksums.txt")), + ); + for (const line of checksumsText.trim().split("\n")) { + const [hash, file] = line.split(/\s+/) as [string, string]; + checksums.set(file, hash); + } -const baseUrl = local - ? `file://${distDir}` - : `https://github.com/${repo}/releases/download/v${version}`; + const sha = Effect.fnUntraced(function* (file: string) { + const hash = checksums.get(file); + if (!hash) return yield* new FormulaError({ message: `Checksum not found for ${file}` }); + return hash; + }); -const formula = `class ${className} < Formula + const baseUrl = local + ? `file://${distDir}` + : `https://github.com/${repo}/releases/download/v${version}`; + + const formula = `class ${className} < Formula desc "Supabase CLI" homepage "https://supabase.com" version "${version}" @@ -76,20 +87,20 @@ const formula = `class ${className} < Formula on_macos do if Hardware::CPU.arm? url "${baseUrl}/supabase_${version}_darwin_arm64.tar.gz" - sha256 "${sha(`supabase_${version}_darwin_arm64.tar.gz`)}" + sha256 "${yield* sha(`supabase_${version}_darwin_arm64.tar.gz`)}" else url "${baseUrl}/supabase_${version}_darwin_amd64.tar.gz" - sha256 "${sha(`supabase_${version}_darwin_amd64.tar.gz`)}" + sha256 "${yield* sha(`supabase_${version}_darwin_amd64.tar.gz`)}" end end on_linux do if Hardware::CPU.arm? url "${baseUrl}/supabase_${version}_linux_arm64.tar.gz" - sha256 "${sha(`supabase_${version}_linux_arm64.tar.gz`)}" + sha256 "${yield* sha(`supabase_${version}_linux_arm64.tar.gz`)}" else url "${baseUrl}/supabase_${version}_linux_amd64.tar.gz" - sha256 "${sha(`supabase_${version}_linux_amd64.tar.gz`)}" + sha256 "${yield* sha(`supabase_${version}_linux_amd64.tar.gz`)}" end end @@ -103,43 +114,63 @@ ${installBlock} end `; -const formulaFileName = `${name}.rb`; -const formulaOut = path.join(distDir, formulaFileName); -await writeFile(formulaOut, formula); -console.log(`Formula written to ${formulaOut}`); + const formulaFileName = `${name}.rb`; + const formulaOut = path.join(distDir, formulaFileName); + yield* fs.writeFileString(formulaOut, formula); + yield* Console.log(`Formula written to ${formulaOut}`); -if (local || dryRun) { - console.log(formula); - process.exit(0); -} + if (local || dryRun) { + yield* Console.log(formula); + return yield* Effect.sync(() => process.exit(0)); + } -async function hasStagedChanges(repoDir: string, repoPath: string): Promise { - const diff = - await $`git -C ${repoDir} diff --cached --quiet --exit-code -- ${repoPath}`.nothrow(); - if (diff.exitCode === 0) return false; - if (diff.exitCode === 1) return true; - throw new Error(`Failed to inspect staged changes for ${repoPath}`); -} + const hasStagedChanges = Effect.fnUntraced(function* (repoDir: string, repoPath: string) { + const diff = yield* shell(() => + $`git -C ${repoDir} diff --cached --quiet --exit-code -- ${repoPath}`.nothrow(), + ); + if (diff.exitCode === 0) return false; + if (diff.exitCode === 1) return true; + return yield* new FormulaError({ message: `Failed to inspect staged changes for ${repoPath}` }); + }); + + const publishToTap = (tmpDir: string) => + Effect.gen(function* () { + const tapUrl = `https://github.com/${tap}.git`; + yield* shell(() => $`git clone ${tapUrl} ${tmpDir}`); + + const formulaDir = path.join(tmpDir, "Formula"); + yield* shell(() => $`mkdir -p ${formulaDir}`); + const tapFormulaPath = path.join(formulaDir, formulaFileName); + const tapFormulaRepoPath = `Formula/${formulaFileName}`; + yield* fs.writeFileString(tapFormulaPath, formula); + + yield* shell(() => $`git -C ${tmpDir} add ${tapFormulaRepoPath}`); + if (yield* hasStagedChanges(tmpDir, tapFormulaRepoPath)) { + yield* shell(() => $`git -C ${tmpDir} commit -m ${name + " " + version}`); + yield* shell(() => $`git -C ${tmpDir} push`); + yield* Console.log(`Pushed formula update to ${tap}`); + } else { + yield* Console.log(`Formula ${formulaFileName} is already up to date in ${tap}`); + } + }); + /* A failed removal replaces the run's outcome. */ + yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const tmpDir = yield* fs.makeTempDirectory({ prefix: "homebrew-tap-" }); + const outcome = yield* Effect.exit(restore(publishToTap(tmpDir))); + yield* fs.remove(tmpDir, { recursive: true }); + return yield* outcome; + }), + ); +}); -const tmpDir = await mkdtemp(path.join(tmpdir(), "homebrew-tap-")); -try { - const tapUrl = `https://github.com/${tap}.git`; - await $`git clone ${tapUrl} ${tmpDir}`; - - const formulaDir = path.join(tmpDir, "Formula"); - await $`mkdir -p ${formulaDir}`; - const tapFormulaPath = path.join(formulaDir, formulaFileName); - const tapFormulaRepoPath = `Formula/${formulaFileName}`; - await writeFile(tapFormulaPath, formula); - - await $`git -C ${tmpDir} add ${tapFormulaRepoPath}`; - if (await hasStagedChanges(tmpDir, tapFormulaRepoPath)) { - await $`git -C ${tmpDir} commit -m ${name + " " + version}`; - await $`git -C ${tmpDir} push`; - console.log(`Pushed formula update to ${tap}`); - } else { - console.log(`Formula ${formulaFileName} is already up to date in ${tap}`); - } -} finally { - await rm(tmpDir, { recursive: true }); +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + return error.cause !== undefined ? error.cause : new Error(error.message); } + +await Effect.runPromise( + main.pipe( + Effect.catch((error) => Effect.die(thrownValue(error))), + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + ), +); diff --git a/apps/cli/scripts/update-scoop.ts b/apps/cli/scripts/update-scoop.ts index 596f6405f7..cadbd2bdb0 100644 --- a/apps/cli/scripts/update-scoop.ts +++ b/apps/cli/scripts/update-scoop.ts @@ -1,9 +1,8 @@ import { $ } from "bun"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { Console, Data, Effect, FileSystem, Layer, Path, Schema } from "effect"; const { values } = parseArgs({ options: { @@ -16,122 +15,158 @@ const { values } = parseArgs({ }, }); -const version = values.version; -if (!version) { - console.error( - "Usage: bun run scripts/update-scoop.ts --version [--repo ] [--bucket ] [--name ] [--local] [--dry-run]", +class ShellFailed extends Data.TaggedError("ShellFailed")<{ readonly cause: unknown }> {} + +class ManifestError extends Data.TaggedError("ManifestError")<{ readonly message: string }> {} + +const shell = (command: () => PromiseLike) => + Effect.tryPromise({ try: command, catch: (cause) => new ShellFailed({ cause }) }); + +const encodeManifest = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown, { space: 4 })); + +const main = Effect.gen(function* () { + const version = values.version; + if (!version) { + yield* Console.error( + "Usage: bun run scripts/update-scoop.ts --version [--repo ] [--bucket ] [--name ] [--local] [--dry-run]", + ); + return yield* Effect.sync(() => process.exit(1)); + } + + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const repo = values.repo!; + const bucket = values.bucket!; + const name = values.name!; + const local = values.local!; + const dryRun = values["dry-run"]!; + + // The shipped binary is always `supabase.exe`; only the manifest filename differs (e.g. + // `supabase-beta.json`), so stable and beta can coexist in the same bucket. + const binEntry = "supabase.exe"; + const root = path.resolve(import.meta.dir, "../../.."); + const distDir = path.join(root, "dist"); + + const checksums = new Map(); + const checksumsText = new TextDecoder("utf-8", { ignoreBOM: true }).decode( + yield* fs.readFile(path.join(distDir, "checksums.txt")), ); - process.exit(1); -} + for (const line of checksumsText.trim().split("\n")) { + const [hash, file] = line.split(/\s+/) as [string, string]; + checksums.set(file, hash); + } -const repo = values.repo!; -const bucket = values.bucket!; -const name = values.name!; -const local = values.local!; -const dryRun = values["dry-run"]!; - -// The shipped binary is always `supabase.exe`; only the manifest filename differs (e.g. -// `supabase-beta.json`), so stable and beta can coexist in the same bucket. -const binEntry = "supabase.exe"; -const root = path.resolve(import.meta.dir, "../../.."); -const distDir = path.join(root, "dist"); - -const checksums = new Map(); -const checksumsText = await readFile(path.join(distDir, "checksums.txt"), "utf-8"); -for (const line of checksumsText.trim().split("\n")) { - const [hash, file] = line.split(/\s+/) as [string, string]; - checksums.set(file, hash); -} + const sha = Effect.fnUntraced(function* (file: string) { + const hash = checksums.get(file); + if (!hash) return yield* new ManifestError({ message: `Checksum not found for ${file}` }); + return hash; + }); -function sha(file: string): string { - const hash = checksums.get(file); - if (!hash) throw new Error(`Checksum not found for ${file}`); - return hash; -} + // Scoop supports file:// URLs for local testing + const baseUrl = local + ? `file:///${distDir.replace(/\\/g, "/")}` + : `https://github.com/${repo}/releases/download/v${version}`; -// Scoop supports file:// URLs for local testing -const baseUrl = local - ? `file:///${distDir.replace(/\\/g, "/")}` - : `https://github.com/${repo}/releases/download/v${version}`; - -// Published releases use unversioned Windows tarball names (release-shared.yml copies the -// versioned build); local builds only emit versioned archives, so --local keeps those names. -const amd64Tar = local - ? `supabase_${version}_windows_amd64.tar.gz` - : "supabase_windows_amd64.tar.gz"; -const arm64Tar = local - ? `supabase_${version}_windows_arm64.tar.gz` - : "supabase_windows_arm64.tar.gz"; - -const manifest = { - version, - description: "Supabase CLI", - homepage: "https://supabase.com/", - license: "MIT", - architecture: { - "64bit": { - url: `${baseUrl}/${amd64Tar}`, - hash: sha(`supabase_${version}_windows_amd64.tar.gz`), - }, - arm64: { - url: `${baseUrl}/${arm64Tar}`, - hash: sha(`supabase_${version}_windows_arm64.tar.gz`), - }, - }, - bin: binEntry, - checkver: { - github: `https://github.com/${repo}`, - }, - autoupdate: { + // Published releases use unversioned Windows tarball names (release-shared.yml copies the + // versioned build); local builds only emit versioned archives, so --local keeps those names. + const amd64Tar = local + ? `supabase_${version}_windows_amd64.tar.gz` + : "supabase_windows_amd64.tar.gz"; + const arm64Tar = local + ? `supabase_${version}_windows_arm64.tar.gz` + : "supabase_windows_arm64.tar.gz"; + + const manifest = { + version, + description: "Supabase CLI", + homepage: "https://supabase.com/", + license: "MIT", architecture: { "64bit": { - url: `https://github.com/${repo}/releases/download/v$version/supabase_windows_amd64.tar.gz`, + url: `${baseUrl}/${amd64Tar}`, + hash: yield* sha(`supabase_${version}_windows_amd64.tar.gz`), }, arm64: { - url: `https://github.com/${repo}/releases/download/v$version/supabase_windows_arm64.tar.gz`, + url: `${baseUrl}/${arm64Tar}`, + hash: yield* sha(`supabase_${version}_windows_arm64.tar.gz`), }, }, - hash: { - url: "$baseurl/supabase_$version_checksums.txt", + bin: binEntry, + checkver: { + github: `https://github.com/${repo}`, }, - }, -}; + autoupdate: { + architecture: { + "64bit": { + url: `https://github.com/${repo}/releases/download/v$version/supabase_windows_amd64.tar.gz`, + }, + arm64: { + url: `https://github.com/${repo}/releases/download/v$version/supabase_windows_arm64.tar.gz`, + }, + }, + hash: { + url: "$baseurl/supabase_$version_checksums.txt", + }, + }, + }; -const manifestFileName = `${name}.json`; -const manifestJson = `${JSON.stringify(manifest, null, 4)}\n`; -const manifestOut = path.join(distDir, manifestFileName); -await writeFile(manifestOut, manifestJson); -console.log(`Manifest written to ${manifestOut}`); + const manifestFileName = `${name}.json`; + const manifestJson = `${yield* encodeManifest(manifest)}\n`; + const manifestOut = path.join(distDir, manifestFileName); + yield* fs.writeFileString(manifestOut, manifestJson); + yield* Console.log(`Manifest written to ${manifestOut}`); -if (local || dryRun) { - console.log(manifestJson); - process.exit(0); -} + if (local || dryRun) { + yield* Console.log(manifestJson); + return yield* Effect.sync(() => process.exit(0)); + } -async function hasStagedChanges(repoDir: string, repoPath: string): Promise { - const diff = - await $`git -C ${repoDir} diff --cached --quiet --exit-code -- ${repoPath}`.nothrow(); - if (diff.exitCode === 0) return false; - if (diff.exitCode === 1) return true; - throw new Error(`Failed to inspect staged changes for ${repoPath}`); -} + const hasStagedChanges = Effect.fnUntraced(function* (repoDir: string, repoPath: string) { + const diff = yield* shell(() => + $`git -C ${repoDir} diff --cached --quiet --exit-code -- ${repoPath}`.nothrow(), + ); + if (diff.exitCode === 0) return false; + if (diff.exitCode === 1) return true; + return yield* new ManifestError({ + message: `Failed to inspect staged changes for ${repoPath}`, + }); + }); -const tmpDir = await mkdtemp(path.join(tmpdir(), "scoop-bucket-")); -try { - const bucketUrl = `https://github.com/${bucket}.git`; - await $`git clone ${bucketUrl} ${tmpDir}`; - - const bucketManifestPath = path.join(tmpDir, manifestFileName); - await writeFile(bucketManifestPath, manifestJson); - - await $`git -C ${tmpDir} add ${manifestFileName}`; - if (await hasStagedChanges(tmpDir, manifestFileName)) { - await $`git -C ${tmpDir} commit -m ${name + " " + version}`; - await $`git -C ${tmpDir} push`; - console.log(`Pushed manifest update to ${bucket}`); - } else { - console.log(`Manifest ${manifestFileName} is already up to date in ${bucket}`); - } -} finally { - await rm(tmpDir, { recursive: true }); + const publishToBucket = (tmpDir: string) => + Effect.gen(function* () { + const bucketUrl = `https://github.com/${bucket}.git`; + yield* shell(() => $`git clone ${bucketUrl} ${tmpDir}`); + + const bucketManifestPath = path.join(tmpDir, manifestFileName); + yield* fs.writeFileString(bucketManifestPath, manifestJson); + + yield* shell(() => $`git -C ${tmpDir} add ${manifestFileName}`); + if (yield* hasStagedChanges(tmpDir, manifestFileName)) { + yield* shell(() => $`git -C ${tmpDir} commit -m ${name + " " + version}`); + yield* shell(() => $`git -C ${tmpDir} push`); + yield* Console.log(`Pushed manifest update to ${bucket}`); + } else { + yield* Console.log(`Manifest ${manifestFileName} is already up to date in ${bucket}`); + } + }); + /* A failed removal replaces the run's outcome. */ + yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const tmpDir = yield* fs.makeTempDirectory({ prefix: "scoop-bucket-" }); + const outcome = yield* Effect.exit(restore(publishToBucket(tmpDir))); + yield* fs.remove(tmpDir, { recursive: true }); + return yield* outcome; + }), + ); +}); + +function thrownValue(error: { readonly message: string; readonly cause?: unknown }): unknown { + return error.cause !== undefined ? error.cause : new Error(error.message); } + +await Effect.runPromise( + main.pipe( + Effect.catch((error) => Effect.die(thrownValue(error))), + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + ), +); diff --git a/apps/cli/scripts/upload-release-assets.integration.test.ts b/apps/cli/scripts/upload-release-assets.integration.test.ts index 699cae4bf1..879dd8986a 100644 --- a/apps/cli/scripts/upload-release-assets.integration.test.ts +++ b/apps/cli/scripts/upload-release-assets.integration.test.ts @@ -1,7 +1,16 @@ -import { afterEach, describe, expect, test } from "vitest"; -import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { + Clock, + Config, + ConfigProvider, + Effect, + FileSystem, + Layer, + Option, + Path, + Schema, +} from "effect"; import { fileURLToPath } from "node:url"; import { createSpawnRun, @@ -17,15 +26,13 @@ const asset = { path: "dist/supabase_1.0.0_darwin_arm64.tar.gz", name: "supabase_1.0.0_darwin_arm64.tar.gz", }; -const temporaryDirectories: string[] = []; +const platformLayer = Layer.mergeAll( + BunFileSystem.layer, + BunPath.layer, + ConfigProvider.layer(ConfigProvider.fromEnvRecord(process.env, { preserveEmptyStrings: true })), +); -afterEach(async () => { - await Promise.all( - temporaryDirectories - .splice(0) - .map((directory) => rm(directory, { recursive: true, force: true })), - ); -}); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); // A stand-in `gh` that logs every call, fails or hangs once per marker file, and serves // `release view` from view.json. @@ -54,18 +61,21 @@ fi exit 0 `; -async function fakeGhOnPath() { - const directory = await mkdtemp(path.join(tmpdir(), "upload-release-assets-")); - temporaryDirectories.push(directory); +const fakeGhOnPath = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const directory = yield* fs.makeTempDirectoryScoped({ prefix: "upload-release-assets-" }); const bin = path.join(directory, "bin"); - await mkdir(bin); - await writeFile(path.join(bin, "gh"), fakeGh); - await chmod(path.join(bin, "gh"), 0o755); - const env = { ...process.env, FAKE_GH_DIR: directory, PATH: `${bin}:${process.env.PATH}` }; - const calls = async () => - (await readFile(path.join(directory, "calls.log"), "utf8")).trimEnd().split("\n"); + yield* fs.makeDirectory(bin); + yield* fs.writeFileString(path.join(bin, "gh"), fakeGh); + yield* fs.chmod(path.join(bin, "gh"), 0o755); + const inheritedPath = Option.getOrUndefined(yield* Config.option(Config.String("PATH"))); + const env = { ...process.env, FAKE_GH_DIR: directory, PATH: `${bin}:${inheritedPath}` }; + const calls = fs + .readFileString(path.join(directory, "calls.log")) + .pipe(Effect.map((text) => text.trimEnd().split("\n"))); return { directory, env, calls }; -} +}); function recordingIo(run: ReleaseIo["run"]): { io: ReleaseIo; logs: string[] } { const logs: string[] = []; @@ -73,120 +83,153 @@ function recordingIo(run: ReleaseIo["run"]): { io: ReleaseIo; logs: string[] } { logs, io: { run, - fileExists: async () => true, - sleep: () => Promise.resolve(), - log: (line) => { - logs.push(line); - }, + fileExists: () => Effect.succeed(true), + sleep: () => Effect.void, + log: (line) => + Effect.sync(() => { + logs.push(line); + }), }, }; } describe("upload-release-assets against a fake gh", () => { - test("kills an upload that outlives the timeout and succeeds on the retry", async () => { - const { directory, env, calls } = await fakeGhOnPath(); - await writeFile(path.join(directory, `hang-once-${asset.name}`), ""); - const { io, logs } = recordingIo(createSpawnRun(env)); - const startedAt = Date.now(); - - await uploadAssets("v1.0.0", [asset], io, { - maxAttempts: 2, - timeoutMs: 5_000, - backoffMs: () => 0, - }); - - const elapsed = Date.now() - startedAt; - expect(elapsed).toBeGreaterThanOrEqual(5_000); - expect(elapsed).toBeLessThan(25_000); - expect(await calls()).toEqual([ - `release upload v1.0.0 ${asset.path} --clobber`, - `release upload v1.0.0 ${asset.path} --clobber`, - ]); - expect(logs).toEqual([ - `Upload of ${asset.name} failed on attempt 1 (timed out after 5s); retrying in 0s.`, - `Uploaded ${asset.name} (attempt 2).`, - ]); - }, 40_000); - - test("kills an upload that ignores SIGTERM once the grace period passes", async () => { - const { directory, env, calls } = await fakeGhOnPath(); - await writeFile(path.join(directory, `ignore-term-once-${asset.name}`), ""); - const { io, logs } = recordingIo(createSpawnRun(env)); - const startedAt = Date.now(); - - await uploadAssets("v1.0.0", [asset], io, { - maxAttempts: 2, - timeoutMs: 5_000, - backoffMs: () => 0, - }); - - const elapsed = Date.now() - startedAt; - expect(elapsed).toBeGreaterThanOrEqual(5_000 + KILL_GRACE_MS); - expect(elapsed).toBeLessThan(25_000); - expect(await calls()).toHaveLength(2); - expect(logs[0]).toBe( - `Upload of ${asset.name} failed on attempt 1 (timed out after 5s); retrying in 0s.`, - ); - }, 40_000); - - test("surfaces gh's stderr for a failed attempt and retries it", async () => { - const { directory, env, calls } = await fakeGhOnPath(); - await writeFile(path.join(directory, `fail-once-${asset.name}`), ""); - const { io, logs } = recordingIo(createSpawnRun(env)); - - await uploadAssets("v1.0.0", [asset], io, { - maxAttempts: 2, - timeoutMs: 5_000, - backoffMs: () => 0, - }); - - expect(await calls()).toHaveLength(2); - expect(logs[0]).toBe( - `Upload of ${asset.name} failed on attempt 1 (exit 1: HTTP 500: Error saving asset); retrying in 0s.`, - ); - }); - - test("reads uploaded asset names from gh release view", async () => { - const { directory, env } = await fakeGhOnPath(); - await writeFile( - path.join(directory, "view.json"), - JSON.stringify({ - assets: [ - { name: "install", state: "uploaded" }, - { name: "checksums.txt", state: "starter" }, - ], - }), - ); - const { io } = recordingIo(createSpawnRun(env)); - - await expect(uploadedAssetNames("v1.0.0", io)).resolves.toEqual(["install"]); - }); - - test("runs as a command and uploads all 22 assets from the working directory", async () => { - const { directory, env, calls } = await fakeGhOnPath(); - const workdir = path.join(directory, "work"); - await mkdir(path.join(workdir, "dist"), { recursive: true }); - for (const asset of releaseAssets("1.0.0")) { - await writeFile(path.join(workdir, asset.path), asset.name); - } - const bunExecutable = Bun.which("bun"); - if (!bunExecutable) throw new Error("Bun executable not found"); - - const child = Bun.spawn([bunExecutable, scriptPath, "upload", "--version", "1.0.0"], { - cwd: workdir, - env, - stdout: "pipe", - stderr: "pipe", - }); - const [exitCode, stdout, stderr] = await Promise.all([ - child.exited, - new Response(child.stdout).text(), - new Response(child.stderr).text(), - ]); - - expect(exitCode, stderr).toBe(0); - expect(await calls()).toHaveLength(22); - expect(stdout.trim().split("\n")).toHaveLength(22); - expect(stdout).toContain("Uploaded install (attempt 1)."); - }, 20_000); + it.live( + "kills an upload that outlives the timeout and succeeds on the retry", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { directory, env, calls } = yield* fakeGhOnPath; + yield* fs.writeFileString(path.join(directory, `hang-once-${asset.name}`), ""); + const { io, logs } = recordingIo(createSpawnRun(env)); + const startedAt = yield* Clock.currentTimeMillis; + + yield* uploadAssets("v1.0.0", [asset], io, { + maxAttempts: 2, + timeoutMs: 5_000, + backoffMs: () => 0, + }); + + const elapsed = (yield* Clock.currentTimeMillis) - startedAt; + expect(elapsed).toBeGreaterThanOrEqual(5_000); + expect(elapsed).toBeLessThan(25_000); + expect(yield* calls).toEqual([ + `release upload v1.0.0 ${asset.path} --clobber`, + `release upload v1.0.0 ${asset.path} --clobber`, + ]); + expect(logs).toEqual([ + `Upload of ${asset.name} failed on attempt 1 (timed out after 5s); retrying in 0s.`, + `Uploaded ${asset.name} (attempt 2).`, + ]); + }).pipe(Effect.provide(platformLayer)), + 40_000, + ); + + it.live( + "kills an upload that ignores SIGTERM once the grace period passes", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { directory, env, calls } = yield* fakeGhOnPath; + yield* fs.writeFileString(path.join(directory, `ignore-term-once-${asset.name}`), ""); + const { io, logs } = recordingIo(createSpawnRun(env)); + const startedAt = yield* Clock.currentTimeMillis; + + yield* uploadAssets("v1.0.0", [asset], io, { + maxAttempts: 2, + timeoutMs: 5_000, + backoffMs: () => 0, + }); + + const elapsed = (yield* Clock.currentTimeMillis) - startedAt; + expect(elapsed).toBeGreaterThanOrEqual(5_000 + KILL_GRACE_MS); + expect(elapsed).toBeLessThan(25_000); + expect(yield* calls).toHaveLength(2); + expect(logs[0]).toBe( + `Upload of ${asset.name} failed on attempt 1 (timed out after 5s); retrying in 0s.`, + ); + }).pipe(Effect.provide(platformLayer)), + 40_000, + ); + + it.live("surfaces gh's stderr for a failed attempt and retries it", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { directory, env, calls } = yield* fakeGhOnPath; + yield* fs.writeFileString(path.join(directory, `fail-once-${asset.name}`), ""); + const { io, logs } = recordingIo(createSpawnRun(env)); + + yield* uploadAssets("v1.0.0", [asset], io, { + maxAttempts: 2, + timeoutMs: 5_000, + backoffMs: () => 0, + }); + + expect(yield* calls).toHaveLength(2); + expect(logs[0]).toBe( + `Upload of ${asset.name} failed on attempt 1 (exit 1: HTTP 500: Error saving asset); retrying in 0s.`, + ); + }).pipe(Effect.provide(platformLayer)), + ); + + it.live("reads uploaded asset names from gh release view", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { directory, env } = yield* fakeGhOnPath; + yield* fs.writeFileString( + path.join(directory, "view.json"), + yield* encodeJson({ + assets: [ + { name: "install", state: "uploaded" }, + { name: "checksums.txt", state: "starter" }, + ], + }), + ); + const { io } = recordingIo(createSpawnRun(env)); + + expect(yield* uploadedAssetNames("v1.0.0", io)).toEqual(["install"]); + }).pipe(Effect.provide(platformLayer)), + ); + + it.live( + "runs as a command and uploads all 22 assets from the working directory", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { directory, env, calls } = yield* fakeGhOnPath; + const workdir = path.join(directory, "work"); + yield* fs.makeDirectory(path.join(workdir, "dist"), { recursive: true }); + for (const asset of releaseAssets("1.0.0")) { + yield* fs.writeFileString(path.join(workdir, asset.path), asset.name); + } + const bunExecutable = Bun.which("bun"); + if (!bunExecutable) throw new Error("Bun executable not found"); + + const child = Bun.spawn([bunExecutable, scriptPath, "upload", "--version", "1.0.0"], { + cwd: workdir, + env, + stdout: "pipe", + stderr: "pipe", + }); + const [exitCode, stdout, stderr] = yield* Effect.all( + [ + Effect.promise(() => child.exited), + Effect.promise(() => new Response(child.stdout).text()), + Effect.promise(() => new Response(child.stderr).text()), + ], + { concurrency: "unbounded" }, + ); + + expect(exitCode, stderr).toBe(0); + expect(yield* calls).toHaveLength(22); + expect(stdout.trim().split("\n")).toHaveLength(22); + expect(stdout).toContain("Uploaded install (attempt 1)."); + }).pipe(Effect.provide(platformLayer)), + 20_000, + ); }); diff --git a/apps/cli/scripts/upload-release-assets.ts b/apps/cli/scripts/upload-release-assets.ts index a0138e12ed..c9b16b98a4 100644 --- a/apps/cli/scripts/upload-release-assets.ts +++ b/apps/cli/scripts/upload-release-assets.ts @@ -1,5 +1,6 @@ import { parseArgs } from "node:util"; import process from "node:process"; +import { Console, Data, Effect, Fiber } from "effect"; export interface ReleaseAsset { path: string; @@ -42,11 +43,25 @@ export interface RunResult { timedOut: boolean; } +class ReleaseAssetsError extends Data.TaggedError("ReleaseAssetsError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +const toReleaseAssetsError = (cause: unknown) => + new ReleaseAssetsError({ + message: cause instanceof Error ? cause.message : String(cause), + cause, + }); + export interface ReleaseIo { - run: (argv: string[], options: { timeoutMs: number }) => Promise; - fileExists: (path: string) => Promise; - sleep: (ms: number) => Promise; - log: (line: string) => void; + run: ( + argv: string[], + options: { timeoutMs: number }, + ) => Effect.Effect; + fileExists: (path: string) => Effect.Effect; + sleep: (ms: number) => Effect.Effect; + log: (line: string) => Effect.Effect; } export interface RetryPolicy { @@ -71,38 +86,40 @@ function describeFailure(result: RunResult, timeoutMs: number): string { } /** Uploads each asset in turn, retrying per asset, and throws on the first asset that never lands. */ -export async function uploadAssets( +export const uploadAssets = Effect.fnUntraced(function* ( tag: string, assets: ReleaseAsset[], io: ReleaseIo, policy: RetryPolicy = defaultRetryPolicy, -): Promise { +) { for (const asset of assets) { - if (!(await io.fileExists(asset.path))) { - throw new Error(`Release asset ${asset.path} does not exist.`); + if (!(yield* io.fileExists(asset.path))) { + return yield* new ReleaseAssetsError({ + message: `Release asset ${asset.path} does not exist.`, + }); } for (let attempt = 1; ; attempt++) { - const result = await io.run(["gh", "release", "upload", tag, asset.path, "--clobber"], { + const result = yield* io.run(["gh", "release", "upload", tag, asset.path, "--clobber"], { timeoutMs: policy.timeoutMs, }); if (result.exitCode === 0) { - io.log(`Uploaded ${asset.name} (attempt ${attempt}).`); + yield* io.log(`Uploaded ${asset.name} (attempt ${attempt}).`); break; } const failure = describeFailure(result, policy.timeoutMs); if (attempt >= policy.maxAttempts) { - throw new Error( - `Upload of ${asset.name} to ${tag} failed after ${attempt} attempts (${failure}).`, - ); + return yield* new ReleaseAssetsError({ + message: `Upload of ${asset.name} to ${tag} failed after ${attempt} attempts (${failure}).`, + }); } const delayMs = policy.backoffMs(attempt); - io.log( + yield* io.log( `Upload of ${asset.name} failed on attempt ${attempt} (${failure}); retrying in ${delayMs / 1000}s.`, ); - await io.sleep(delayMs); + yield* io.sleep(delayMs); } } -} +}); interface ReleaseAssetView { name: string; @@ -110,17 +127,22 @@ interface ReleaseAssetView { } /** Names of the release's assets that GitHub reports as fully uploaded. */ -export async function uploadedAssetNames(tag: string, io: ReleaseIo): Promise { - const result = await io.run(["gh", "release", "view", tag, "--json", "assets"], { +export const uploadedAssetNames = Effect.fnUntraced(function* (tag: string, io: ReleaseIo) { + const result = yield* io.run(["gh", "release", "view", tag, "--json", "assets"], { timeoutMs: 60_000, }); if (result.exitCode !== 0) { - throw new Error(`Could not read assets of ${tag} (${describeFailure(result, 60_000)}).`); + return yield* new ReleaseAssetsError({ + message: `Could not read assets of ${tag} (${describeFailure(result, 60_000)}).`, + }); } - return parseAssetView(result.stdout, tag) + return (yield* Effect.try({ + try: () => parseAssetView(result.stdout, tag), + catch: toReleaseAssetsError, + })) .filter((asset) => asset.state === "uploaded") .map((asset) => asset.name); -} +}); function isAssetView(value: unknown): value is ReleaseAssetView { return ( @@ -158,37 +180,41 @@ const usage = `Usage: bun apps/cli/scripts/upload-release-assets.ts , one at a time with retries. verify Fail unless every expected asset is present on v and reported as uploaded.`; -export async function main(argv: string[], io: ReleaseIo): Promise { - const { values, positionals } = parseArgs({ - args: argv, - options: { version: { type: "string" } }, - allowPositionals: true, +export const main = Effect.fnUntraced(function* (argv: string[], io: ReleaseIo) { + const { values, positionals } = yield* Effect.try({ + try: () => + parseArgs({ + args: argv, + options: { version: { type: "string" } }, + allowPositionals: true, + }), + catch: toReleaseAssetsError, }); const [command] = positionals; if (!values.version || (command !== "upload" && command !== "verify")) { - io.log(usage); + yield* io.log(usage); return 2; } const tag = `v${values.version}`; const assets = releaseAssets(values.version); if (command === "upload") { - await uploadAssets(tag, assets, io); + yield* uploadAssets(tag, assets, io); return 0; } const missing = missingAssets( assets.map((asset) => asset.name), - await uploadedAssetNames(tag, io), + yield* uploadedAssetNames(tag, io), ); if (missing.length > 0) { - io.log(`::error::Release ${tag} is missing assets or has incomplete uploads:`); - for (const name of missing) io.log(` ${name}`); + yield* io.log(`::error::Release ${tag} is missing assets or has incomplete uploads:`); + for (const name of missing) yield* io.log(` ${name}`); return 1; } - io.log(`All ${assets.length} expected assets are present on ${tag}.`); + yield* io.log(`All ${assets.length} expected assets are present on ${tag}.`); return 0; -} +}); /** * Time a timed-out command gets to exit on SIGTERM before it is killed outright, and the bound on @@ -196,56 +222,67 @@ export async function main(argv: string[], io: ReleaseIo): Promise { */ export const KILL_GRACE_MS = 5_000; -function within(promise: Promise, ms: number, fallback: T): Promise { - let timer: ReturnType | undefined; - const expiry = new Promise((resolve) => { - timer = setTimeout(() => resolve(fallback), ms); - }); - return Promise.race([promise, expiry]).finally(() => clearTimeout(timer)); -} +const within = (fiber: Fiber.Fiber, ms: number, fallback: string) => + Fiber.join(fiber).pipe( + Effect.timeoutOrElse({ duration: ms, orElse: () => Effect.succeed(fallback) }), + ); /** Runs a command with the given environment and terminates it once the timeout elapses. */ export function createSpawnRun( env: Record = process.env, ): ReleaseIo["run"] { - return async (argv, { timeoutMs }) => { - const child = Bun.spawn(argv, { env, stdout: "pipe", stderr: "pipe" }); + return Effect.fnUntraced(function* (argv: string[], { timeoutMs }: { timeoutMs: number }) { + const child = yield* Effect.try({ + try: () => Bun.spawn(argv, { env, stdout: "pipe", stderr: "pipe" }), + catch: toReleaseAssetsError, + }); let timedOut = false; - let killTimer: ReturnType | undefined; - const timer = setTimeout(() => { - timedOut = true; - child.kill("SIGTERM"); - killTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS); - }, timeoutMs); + yield* Effect.sleep(timeoutMs).pipe( + Effect.andThen( + Effect.sync(() => { + timedOut = true; + child.kill("SIGTERM"); + }), + ), + Effect.andThen(Effect.sleep(KILL_GRACE_MS)), + Effect.andThen(Effect.sync(() => child.kill("SIGKILL"))), + Effect.forkScoped({ startImmediately: true }), + ); // Start draining before the child exits so a chatty child never blocks on a full pipe. - const stdoutText = new Response(child.stdout).text(); - const stderrText = new Response(child.stderr).text(); - try { - const exitCode = await child.exited; - const [stdout, stderr] = await Promise.all([ - within(stdoutText, KILL_GRACE_MS, ""), - within(stderrText, KILL_GRACE_MS, ""), - ]); - return { exitCode, stdout, stderr, timedOut }; - } finally { - clearTimeout(timer); - clearTimeout(killTimer); - } - }; + const stdoutText = yield* Effect.tryPromise({ + try: () => new Response(child.stdout).text(), + catch: toReleaseAssetsError, + }).pipe(Effect.forkScoped({ startImmediately: true })); + const stderrText = yield* Effect.tryPromise({ + try: () => new Response(child.stderr).text(), + catch: toReleaseAssetsError, + }).pipe(Effect.forkScoped({ startImmediately: true })); + const exitCode = yield* Effect.tryPromise({ + try: () => child.exited, + catch: toReleaseAssetsError, + }); + const [stdout, stderr] = yield* Effect.all( + [within(stdoutText, KILL_GRACE_MS, ""), within(stderrText, KILL_GRACE_MS, "")], + { concurrency: "unbounded" }, + ); + return { exitCode, stdout, stderr, timedOut }; + }, Effect.scoped); } export const processIo: ReleaseIo = { run: createSpawnRun(), - fileExists: (path) => Bun.file(path).exists(), - sleep: (ms) => Bun.sleep(ms), - log: (line) => console.log(line), + fileExists: (path) => + Effect.tryPromise({ try: () => Bun.file(path).exists(), catch: toReleaseAssetsError }), + sleep: (ms) => Effect.sleep(ms), + log: (line) => Console.log(line), }; if (import.meta.main) { - try { - process.exit(await main(process.argv.slice(2), processIo)); - } catch (cause) { - console.error(`::error::${cause instanceof Error ? cause.message : String(cause)}`); - process.exit(1); - } + process.exit( + await Effect.runPromise( + main(process.argv.slice(2), processIo).pipe( + Effect.catch((error) => Console.error(`::error::${error.message}`).pipe(Effect.as(1))), + ), + ), + ); } diff --git a/apps/cli/scripts/upload-release-assets.unit.test.ts b/apps/cli/scripts/upload-release-assets.unit.test.ts index 8c4c99d88c..58f41fc537 100644 --- a/apps/cli/scripts/upload-release-assets.unit.test.ts +++ b/apps/cli/scripts/upload-release-assets.unit.test.ts @@ -1,3 +1,5 @@ +import { it } from "@effect/vitest"; +import { Effect, Schema } from "effect"; import { describe, expect, test } from "vitest"; import { main, @@ -19,6 +21,8 @@ const failed: RunResult = { }; const timedOut: RunResult = { exitCode: null, stdout: "", stderr: "", timedOut: true }; +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); + const fastPolicy: RetryPolicy = { maxAttempts: 3, timeoutMs: 1_000, @@ -32,18 +36,21 @@ function fakeIo( ) { const state = { runs: [] as string[][], sleeps: [] as number[], logs: [] as string[] }; const io: ReleaseIo = { - run: async (argv) => { - state.runs.push(argv); - const target = (argv[2] === "upload" ? argv[4] : argv[3]) ?? ""; - return script[target]?.shift() ?? ok; - }, - fileExists: async (path) => !options.missingFiles?.includes(path), - sleep: async (ms) => { - state.sleeps.push(ms); - }, - log: (line) => { - state.logs.push(line); - }, + run: (argv) => + Effect.sync(() => { + state.runs.push(argv); + const target = (argv[2] === "upload" ? argv[4] : argv[3]) ?? ""; + return script[target]?.shift() ?? ok; + }), + fileExists: (path) => Effect.sync(() => !options.missingFiles?.includes(path)), + sleep: (ms) => + Effect.sync(() => { + state.sleeps.push(ms); + }), + log: (line) => + Effect.sync(() => { + state.logs.push(line); + }), }; return { io, state }; } @@ -76,104 +83,123 @@ describe("uploadAssets", () => { const third = { path: "install", name: "install" }; const assets = [first, second, third]; - test("uploads every asset once with --clobber when nothing fails", async () => { - const { io, state } = fakeIo(); - - await uploadAssets("v1.0.0", assets, io, fastPolicy); - - expect(state.runs).toEqual( - assets.map((asset) => ["gh", "release", "upload", "v1.0.0", asset.path, "--clobber"]), - ); - expect(state.sleeps).toEqual([]); - expect(state.logs.filter((line) => line.startsWith("Uploaded "))).toHaveLength(3); - }); - - test("retries a failed asset with growing pauses and moves on once it lands", async () => { - const { io, state } = fakeIo({ [second.path]: [failed, failed] }); - - await uploadAssets("v1.0.0", assets, io, fastPolicy); - - const secondAttempts = state.runs.filter((argv) => argv[4] === second.path); - expect(secondAttempts).toHaveLength(3); - expect(state.sleeps).toEqual([10, 20]); - expect(state.logs).toContain( - `Upload of ${second.name} failed on attempt 1 (exit 1: HTTP 500: Error saving asset); retrying in 0.01s.`, - ); - expect(state.logs).toContain(`Uploaded ${first.name} (attempt 1).`); - expect(state.logs).toContain(`Uploaded ${second.name} (attempt 3).`); - }); - - test("gives up on an asset after the last attempt and does not touch later assets", async () => { - const { io, state } = fakeIo({ [second.path]: [failed, failed, failed] }); - - await expect(uploadAssets("v1.0.0", assets, io, fastPolicy)).rejects.toThrow( - `Upload of ${second.name} to v1.0.0 failed after 3 attempts (exit 1: HTTP 500: Error saving asset).`, - ); - - expect(state.runs.filter((argv) => argv[4] === third.path)).toHaveLength(0); - expect(state.sleeps).toEqual([10, 20]); - }); - - test("treats a timed-out upload as a failed attempt", async () => { - const { io, state } = fakeIo({ [first.path]: [timedOut] }); - - await uploadAssets("v1.0.0", [first], io, fastPolicy); - - expect(state.runs).toHaveLength(2); - expect(state.logs[0]).toBe( - `Upload of ${first.name} failed on attempt 1 (timed out after 1s); retrying in 0.01s.`, - ); - }); - - test("fails before calling gh when an asset file is missing", async () => { - const { io, state } = fakeIo({}, { missingFiles: [first.path] }); - - await expect(uploadAssets("v1.0.0", [first], io, fastPolicy)).rejects.toThrow( - `Release asset ${first.path} does not exist.`, - ); - expect(state.runs).toEqual([]); - }); + it.effect("uploads every asset once with --clobber when nothing fails", () => + Effect.gen(function* () { + const { io, state } = fakeIo(); + + yield* uploadAssets("v1.0.0", assets, io, fastPolicy); + + expect(state.runs).toEqual( + assets.map((asset) => ["gh", "release", "upload", "v1.0.0", asset.path, "--clobber"]), + ); + expect(state.sleeps).toEqual([]); + expect(state.logs.filter((line) => line.startsWith("Uploaded "))).toHaveLength(3); + }), + ); + + it.effect("retries a failed asset with growing pauses and moves on once it lands", () => + Effect.gen(function* () { + const { io, state } = fakeIo({ [second.path]: [failed, failed] }); + + yield* uploadAssets("v1.0.0", assets, io, fastPolicy); + + const secondAttempts = state.runs.filter((argv) => argv[4] === second.path); + expect(secondAttempts).toHaveLength(3); + expect(state.sleeps).toEqual([10, 20]); + expect(state.logs).toContain( + `Upload of ${second.name} failed on attempt 1 (exit 1: HTTP 500: Error saving asset); retrying in 0.01s.`, + ); + expect(state.logs).toContain(`Uploaded ${first.name} (attempt 1).`); + expect(state.logs).toContain(`Uploaded ${second.name} (attempt 3).`); + }), + ); + + it.effect("gives up on an asset after the last attempt and does not touch later assets", () => + Effect.gen(function* () { + const { io, state } = fakeIo({ [second.path]: [failed, failed, failed] }); + + const error = yield* Effect.flip(uploadAssets("v1.0.0", assets, io, fastPolicy)); + expect(error.message).toContain( + `Upload of ${second.name} to v1.0.0 failed after 3 attempts (exit 1: HTTP 500: Error saving asset).`, + ); + + expect(state.runs.filter((argv) => argv[4] === third.path)).toHaveLength(0); + expect(state.sleeps).toEqual([10, 20]); + }), + ); + + it.effect("treats a timed-out upload as a failed attempt", () => + Effect.gen(function* () { + const { io, state } = fakeIo({ [first.path]: [timedOut] }); + + yield* uploadAssets("v1.0.0", [first], io, fastPolicy); + + expect(state.runs).toHaveLength(2); + expect(state.logs[0]).toBe( + `Upload of ${first.name} failed on attempt 1 (timed out after 1s); retrying in 0.01s.`, + ); + }), + ); + + it.effect("fails before calling gh when an asset file is missing", () => + Effect.gen(function* () { + const { io, state } = fakeIo({}, { missingFiles: [first.path] }); + + const error = yield* Effect.flip(uploadAssets("v1.0.0", [first], io, fastPolicy)); + expect(error.message).toContain(`Release asset ${first.path} does not exist.`); + expect(state.runs).toEqual([]); + }), + ); }); describe("uploadedAssetNames", () => { - test("keeps only assets GitHub reports as uploaded", async () => { - const view = { - assets: [ - { name: "checksums.txt", state: "uploaded" }, - { name: "install", state: "starter" }, - ], - }; - const { io, state } = fakeIo({ "v1.0.0": [{ ...ok, stdout: JSON.stringify(view) }] }); - - await expect(uploadedAssetNames("v1.0.0", io)).resolves.toEqual(["checksums.txt"]); - expect(state.runs).toEqual([["gh", "release", "view", "v1.0.0", "--json", "assets"]]); - }); - - test("fails when the release cannot be read", async () => { - const { io } = fakeIo({ "v1.0.0": [{ ...failed, stderr: "release not found\n" }] }); - - await expect(uploadedAssetNames("v1.0.0", io)).rejects.toThrow( - "Could not read assets of v1.0.0 (exit 1: release not found).", - ); - }); - - test("names the release when gh prints something other than JSON", async () => { - const { io } = fakeIo({ "v1.0.0": [{ ...ok, stdout: "gh: rate limited\n" }] }); - - await expect(uploadedAssetNames("v1.0.0", io)).rejects.toThrow( - "Could not read assets of v1.0.0: gh returned invalid JSON.", - ); - }); - - test("names the release when the JSON has no usable assets array", async () => { - const { io } = fakeIo({ - "v1.0.0": [{ ...ok, stdout: JSON.stringify({ assets: [{ name: "install" }] }) }], - }); - - await expect(uploadedAssetNames("v1.0.0", io)).rejects.toThrow( - "Could not read assets of v1.0.0: gh output has no assets array with name and state.", - ); - }); + it.effect("keeps only assets GitHub reports as uploaded", () => + Effect.gen(function* () { + const view = { + assets: [ + { name: "checksums.txt", state: "uploaded" }, + { name: "install", state: "starter" }, + ], + }; + const { io, state } = fakeIo({ "v1.0.0": [{ ...ok, stdout: yield* encodeJson(view) }] }); + + expect(yield* uploadedAssetNames("v1.0.0", io)).toEqual(["checksums.txt"]); + expect(state.runs).toEqual([["gh", "release", "view", "v1.0.0", "--json", "assets"]]); + }), + ); + + it.effect("fails when the release cannot be read", () => + Effect.gen(function* () { + const { io } = fakeIo({ "v1.0.0": [{ ...failed, stderr: "release not found\n" }] }); + + const error = yield* Effect.flip(uploadedAssetNames("v1.0.0", io)); + expect(error.message).toContain( + "Could not read assets of v1.0.0 (exit 1: release not found).", + ); + }), + ); + + it.effect("names the release when gh prints something other than JSON", () => + Effect.gen(function* () { + const { io } = fakeIo({ "v1.0.0": [{ ...ok, stdout: "gh: rate limited\n" }] }); + + const error = yield* Effect.flip(uploadedAssetNames("v1.0.0", io)); + expect(error.message).toContain("Could not read assets of v1.0.0: gh returned invalid JSON."); + }), + ); + + it.effect("names the release when the JSON has no usable assets array", () => + Effect.gen(function* () { + const { io } = fakeIo({ + "v1.0.0": [{ ...ok, stdout: yield* encodeJson({ assets: [{ name: "install" }] }) }], + }); + + const error = yield* Effect.flip(uploadedAssetNames("v1.0.0", io)); + expect(error.message).toContain( + "Could not read assets of v1.0.0: gh output has no assets array with name and state.", + ); + }), + ); }); describe("missingAssets", () => { @@ -189,34 +215,40 @@ describe("main", () => { stdout: JSON.stringify({ assets: names.map((name) => ({ name, state: "uploaded" })) }), }); - test("prints usage for an unknown command or a missing version", async () => { - const { io, state } = fakeIo(); - - await expect(main(["publish", "--version", "1.0.0"], io)).resolves.toBe(2); - await expect(main(["upload"], io)).resolves.toBe(2); - expect(state.runs).toEqual([]); - expect(state.logs[0]).toContain("Usage:"); - }); - - test("verify succeeds when every expected asset is uploaded", async () => { - const names = releaseAssets("1.0.0").map((asset) => asset.name); - const { io, state } = fakeIo({ "v1.0.0": [view(names)] }); - - await expect(main(["verify", "--version", "1.0.0"], io)).resolves.toBe(0); - expect(state.logs).toEqual(["All 22 expected assets are present on v1.0.0."]); - }); - - test("verify fails and names each missing asset", async () => { - const names = releaseAssets("1.0.0") - .map((asset) => asset.name) - .filter((name) => name !== "checksums.txt" && name !== "install"); - const { io, state } = fakeIo({ "v1.0.0": [view(names)] }); - - await expect(main(["verify", "--version", "1.0.0"], io)).resolves.toBe(1); - expect(state.logs).toEqual([ - "::error::Release v1.0.0 is missing assets or has incomplete uploads:", - " checksums.txt", - " install", - ]); - }); + it.effect("prints usage for an unknown command or a missing version", () => + Effect.gen(function* () { + const { io, state } = fakeIo(); + + expect(yield* main(["publish", "--version", "1.0.0"], io)).toBe(2); + expect(yield* main(["upload"], io)).toBe(2); + expect(state.runs).toEqual([]); + expect(state.logs[0]).toContain("Usage:"); + }), + ); + + it.effect("verify succeeds when every expected asset is uploaded", () => + Effect.gen(function* () { + const names = releaseAssets("1.0.0").map((asset) => asset.name); + const { io, state } = fakeIo({ "v1.0.0": [view(names)] }); + + expect(yield* main(["verify", "--version", "1.0.0"], io)).toBe(0); + expect(state.logs).toEqual(["All 22 expected assets are present on v1.0.0."]); + }), + ); + + it.effect("verify fails and names each missing asset", () => + Effect.gen(function* () { + const names = releaseAssets("1.0.0") + .map((asset) => asset.name) + .filter((name) => name !== "checksums.txt" && name !== "install"); + const { io, state } = fakeIo({ "v1.0.0": [view(names)] }); + + expect(yield* main(["verify", "--version", "1.0.0"], io)).toBe(1); + expect(state.logs).toEqual([ + "::error::Release v1.0.0 is missing assets or has incomplete uploads:", + " checksums.txt", + " install", + ]); + }), + ); }); From 11559a9d37d5f26e8682a9d85df0d6119e6bed29 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:43:27 +0000 Subject: [PATCH 28/33] chore(stack): bump storage to v1.84.28-r0 (#7346) This bumps storage from v1.84.25-r0 to v1.84.28-r0. https://github.com/supabase/slim-services/releases/tag/storage-v1.84.28-r0 Planned after supabase/slim-services published v1.84.27-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 41e8d95fb7..fd7af37e56 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.148.2 AS realtime -FROM supabase/storage-api:v1.84.25 AS storage +FROM supabase/storage-api:v1.84.28 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 41e8d95fb7..fd7af37e56 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.148.2 AS realtime -FROM supabase/storage-api:v1.84.25 AS storage +FROM supabase/storage-api:v1.84.28 AS storage FROM supabase/logflare:1.50.15 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index b32b08c0c9..6f3c1633d3 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -343,23 +343,23 @@ const definitions: Readonly> = { storage: definition( "storage", { - upstreamVersion: "v1.84.25", + upstreamVersion: "v1.84.28", revision: 0, image: - "ghcr.io/supabase/cli/storage:v1.84.25-r0@sha256:c28d1ad8c63ce0b487754227f1eb97ca83dc2ccab58c34050fcc46f4ec2198d7", - upstreamImage: "supabase/storage-api:v1.84.25", + "ghcr.io/supabase/cli/storage:v1.84.28-r0@sha256:5a09c78a62bdf99724af5926a15c4621bd284bc8e53db1cb44173998477e596f", + upstreamImage: "supabase/storage-api:v1.84.28", natives: { "darwin-arm64": { - archive: "b7709c1f9d8f494e774313d82651d218cf7cb6833c7598823230289dcfe9a03f", - manifest: "bad18030ee947d49dd1c21350bea7a8f1d58370a6f1a837246266e7f254cf861", + archive: "39a3700203b093a75a47daaf68f6cc8594df0fb507efeeed0b2072549f48447f", + manifest: "732fa4b12d22f7438d0e1b31b55f313be467c8509b50201e8c29b4fd64a1c67b", }, "linux-amd64": { - archive: "07bb85a098b3712947759dbeffd37323c0a1e1856e46313c613d2e0e8068170f", - manifest: "aac9b81078b7f45588a9fb5ed4e3f473b4fc3c95d844258f367df06279e14bf7", + archive: "6d92818489d0d0df814b327a1916017bd4c55abeeb8aa12fc79e0d698c56d712", + manifest: "26a52234fbf2c78c6f0f81de0d9766ceb3482299513990fac2a46d2411e958fc", }, "linux-arm64": { - archive: "58bbaf780f131d4d59da13e25360881846d2dbdca6dbca1973019793192b84bd", - manifest: "47a61a0c93b977ee846f5bedf6969c85af546cc9f485cc03dd7cd441a12c0dc1", + archive: "1428aff9a0a413a4cdfaf0a7c3e3dfbfdf3cdf886dc1807bc5366fa8a5fc8871", + manifest: "4549f3345d65f5f4f6f5ecfa426a8c0bd1677decb524c92437fdd3aed2b8e18c", }, }, }, From 2918d932a3600f5ba321e365e71ae27f7a484cd6 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 27 Oct 2026 14:30:33 +0000 Subject: [PATCH 29/33] fix(stack): reuse the Deno cache when booting Edge Function workers (#7347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary In stack mode, Edge Functions that boot at the same time can fail with `503 BOOT_ERROR`. The logs show `Failed caching npm package … File exists (os error 17)`, or `No such file or directory` when reading a cached package file. This sets `noModuleCache: false` when the stack bootstrap creates user workers, which is what classic `functions serve` already does. ### What's going on `noModuleCache: true` puts the worker's Deno cache into reload mode. Each worker builds its own npm cache, so nothing is shared between workers, and each one downloads every npm package it imports again. When that package is already in the shared `DENO_DIR`, Deno extracts it again in place, over the live copy, deleting and recreating each file. Two workers doing that at the same time collide, and a worker that reads a file in the middle of it finds it missing. ```mermaid flowchart TD boot["Worker boots and imports npm:pkg"] --> flag{"noModuleCache"} flag -- "false" --> cached{"pkg in the cache?"} cached -- "yes" --> reuse["Reuse it"] cached -- "no" --> temp["Extract to a temp dir,
then rename into place"] flag -- "true" --> cached2{"pkg in the cache?"} cached2 -- "no" --> temp cached2 -- "yes" --> inplace["Extract again in place,
over the live copy"] inplace --> fail["Concurrent boots collide
503 BOOT_ERROR"] ``` ```mermaid sequenceDiagram participant A as Worker A participant C as Shared Deno cache participant B as Worker B A->>C: create pkg/README.md (exists, so delete it) B->>C: create pkg/README.md (succeeds) A->>C: create pkg/README.md C-->>A: File exists (os error 17) Note over A: 503 BOOT_ERROR ``` This explains the odd parts of the report: - A package that isn't cached yet goes through the temp dir and rename, which is safe. That's why a cold first round often passes, and why a warm-up request doesn't help. - Native keeps its cache across `stop` and `start`, so it fails much more often than Docker, whose new container starts with an empty cache. - `forceCreate: true` boots a new worker for every request, so every request goes down this path. I've left it as it is, because it's what makes edits show up without a restart. Against Edge Runtime directly, with only this flag changed, 40 of 64 concurrent boots failed with it on and none of 112 failed with it off. ### What changes - npm packages and remote modules are reused from the cache instead of being downloaded and extracted on every boot, so boots are quicker too. - An exact version that isn't cached yet still resolves, because Deno fetches package info again when resolution fails. Floating versions like `npm:x@latest` stay on the cached version until the cache resets (a new container on Docker, `stack destroy` on native). That matches classic `functions serve` and `deno run`. - Local function code is still read from disk on every boot, so edits show up straight away. - A new integration test serves a package from a local npm registry, warms the cache, then boots several workers at once. It checks that they all succeed and that the package is downloaded only once. Earlier bootstraps and classic `functions serve` use `false`. If `true` was there for something I've missed, I'm happy to talk it through. ## Linked issue Closes #7321 (CLI-2850) --- .../functions/generated/serve-main-bundle.ts | 2 +- .../serve-main-bundler.integration.test.ts | 2 +- packages/stack/src/functions/serve.main.ts | 4 +- .../services/Functions.integration.test.ts | 98 ++++++++++++++++ packages/stack/tests/fake-npm-registry.ts | 105 ++++++++++++++++++ 5 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 packages/stack/tests/fake-npm-registry.ts diff --git a/packages/stack/src/functions/generated/serve-main-bundle.ts b/packages/stack/src/functions/generated/serve-main-bundle.ts index 29ab22b1de..9f4dbbaeea 100644 --- a/packages/stack/src/functions/generated/serve-main-bundle.ts +++ b/packages/stack/src/functions/generated/serve-main-bundle.ts @@ -1,3 +1,3 @@ // Generated by packages/stack/scripts/generate-functions-bootstrap.ts; run `pnpm generate`. /** Bundled Edge Runtime main service used when a Functions creation has no bootstrap. */ -export const defaultFunctionsBootstrap = "var eB=Object.defineProperty;var Eo=(e,t)=>{for(var r in t)eB(e,r,{get:t[r],enumerable:!0})};var Z=(e,t)=>{switch(t.length){case 0:return e;case 1:return t[0](e);case 2:return t[1](t[0](e));case 3:return t[2](t[1](t[0](e)));case 4:return t[3](t[2](t[1](t[0](e))));case 5:return t[4](t[3](t[2](t[1](t[0](e)))));case 6:return t[5](t[4](t[3](t[2](t[1](t[0](e))))));case 7:return t[6](t[5](t[4](t[3](t[2](t[1](t[0](e)))))));case 8:return t[7](t[6](t[5](t[4](t[3](t[2](t[1](t[0](e))))))));case 9:return t[8](t[7](t[6](t[5](t[4](t[3](t[2](t[1](t[0](e)))))))));default:{let r=e;for(let n=0,o=t.length;nt(r,...arguments)};switch(e){case 0:case 1:throw new RangeError(`Invalid arity ${e}`);case 2:return function(r,n){return arguments.length>=2?t(r,n):function(o){return t(o,r)}};case 3:return function(r,n,o){return arguments.length>=3?t(r,n,o):function(s){return t(s,r,n)}};default:return function(){if(arguments.length>=e)return t.apply(this,arguments);let r=arguments;return function(n){return t(n,...r)}}}};var R=e=>e;var je=e=>()=>e,Sr=je(!0),yf=je(!1),$g=je(null),bn=je(void 0),$n=bn;function Bg(e,...t){return Z(e,t)}function rC(e,t,r,n,o,s,a,i,c){switch(arguments.length){case 1:return e;case 2:return function(){return t(e.apply(this,arguments))};case 3:return function(){return r(t(e.apply(this,arguments)))};case 4:return function(){return n(r(t(e.apply(this,arguments))))};case 5:return function(){return o(n(r(t(e.apply(this,arguments)))))};case 6:return function(){return s(o(n(r(t(e.apply(this,arguments))))))};case 7:return function(){return a(s(o(n(r(t(e.apply(this,arguments)))))))};case 8:return function(){return i(a(s(o(n(r(t(e.apply(this,arguments))))))))};case 9:return function(){return c(i(a(s(o(n(r(t(e.apply(this,arguments)))))))))}}}function Vr(e){let t=new WeakMap;return r=>{let n=t.get(r);if(n!==void 0)return n;let o=e(r);return t.set(r,o),o}}function Pa(e){let t=new WeakMap;return r=>{let n=t.get(r);if(n!==void 0)return n;let o=e(r);return t.set(r,o),o!==r&&t.set(o,o),o}}var Ft={};Eo(Ft,{AsyncFiberError:()=>vq,AsyncFiberErrorTypeId:()=>Aq,Done:()=>ub,DoneTypeId:()=>yq,ExceededCapacityError:()=>fb,ExceededCapacityErrorTypeId:()=>Cq,IllegalArgumentError:()=>ml,IllegalArgumentErrorTypeId:()=>Iq,InterruptorStackTrace:()=>XS,NoSuchElementError:()=>gq,NoSuchElementErrorTypeId:()=>xq,ReasonTypeId:()=>tq,StackTrace:()=>QS,TimeoutError:()=>Eq,TimeoutErrorTypeId:()=>Sq,TypeId:()=>eq,UnknownError:()=>Rq,UnknownErrorTypeId:()=>Oq,annotate:()=>Pq,annotations:()=>Mq,combine:()=>aq,die:()=>ci,done:()=>Y,empty:()=>nq,fail:()=>oq,filterInterruptors:()=>dq,findDefect:()=>dl,findDie:()=>uq,findError:()=>ab,findErrorOption:()=>ib,findFail:()=>iq,findInterrupt:()=>lq,fromReasons:()=>oa,hasDies:()=>cq,hasFails:()=>sb,hasInterrupts:()=>fq,hasInterruptsOnly:()=>ob,interrupt:()=>sq,interruptors:()=>pq,isAsyncFiberError:()=>kq,isCause:()=>eb,isDieReason:()=>rb,isDone:()=>qc,isExceededCapacityError:()=>wq,isFailReason:()=>na,isIllegalArgumentError:()=>Tq,isInterruptReason:()=>rq,isNoSuchElementError:()=>hq,isReason:()=>tb,isTimeoutError:()=>bq,isUnknownError:()=>_q,makeDieReason:()=>zc,makeFailReason:()=>jc,makeInterruptReason:()=>nb,map:()=>Ss,pretty:()=>cb,prettyErrors:()=>mq,reasonAnnotations:()=>Mm,squash:()=>Pm});var Sf=e=>{let t=new Set(Reflect.ownKeys(e));if(e.constructor===Object)return t;e instanceof Error&&t.delete(\"stack\");let r=Object.getPrototypeOf(e),n=r;for(;n!==null&&n!==Object.prototype;){let o=Reflect.ownKeys(n);for(let s=0;snew Uint8Array(e.buffer,e.byteOffset,e.byteLength);var Ma=0,oC=()=>{Ma++};function br(e){return typeof e==\"string\"}function Ef(e){return typeof e==\"number\"}function ad(e){return typeof e==\"boolean\"}function sC(e){return typeof e==\"bigint\"}function jg(e){return typeof e==\"symbol\"}function id(e){return br(e)||Ef(e)||jg(e)}function Zr(e){return typeof e==\"function\"}function aC(e){return e===void 0}function Io(e){return e!==void 0}function cd(e){return e!=null}function iC(e){return!1}function zg(e){return!0}function fr(e){return typeof e==\"object\"&&e!==null&&!Array.isArray(e)}function If(e){return typeof e==\"object\"&&e!==null||Zr(e)}var k=l(2,(e,t)=>If(e)&&t in e),At=l(2,(e,t)=>k(e,\"_tag\")&&e._tag===t);function cC(e){return e instanceof Error}function as(e){return k(e,Symbol.iterator)||br(e)}var se=\"~effect/Hash\",H=e=>{switch(typeof e){case\"number\":return It(e);case\"bigint\":return Re(e.toString(10));case\"string\":return Re(e);case\"function\":case\"object\":{if(e===null)break;if(e instanceof Date)return Number.isNaN(e.getTime())?Re(\"Invalid Date\"):Re(e.toISOString());if(e instanceof RegExp)return Re(e.toString());{if(lc.has(e))return ud(e);let t=fC.get(e);if(t!==void 0)return t;if(Wg.has(e))return oC(),Re(\"[Circular]\");Wg.add(e);let r=Ma,n;try{se in e?n=e[se]():typeof e==\"function\"?n=ud(e):e instanceof DataView?n=To(bf(e)):Array.isArray(e)||ArrayBuffer.isView(e)?n=To(e):e instanceof Map?n=tB(e):e instanceof Set?n=rB(e):n=Tf(e)}finally{Wg.delete(e)}return r===Ma&&fC.set(e,n),n}}}return qt(lC(Re(String(e))))},ud=e=>(Hg.has(e)||Hg.set(e,qt(Math.random()*4294967296|0)),Hg.get(e)),lC=e=>(e^=e>>>16,e=Math.imul(e,2246822507),e^=e>>>13,e=Math.imul(e,3266489909),e^e>>>16),le=l(2,(e,t)=>lC(Math.imul(e,2654435761)+Math.imul(t,2246822507))),qt=e=>e&3221225471|e>>>1&1073741824;var qg=new DataView(new ArrayBuffer(8)),It=e=>{let t=e|0;return t===e?qt(t):(qg.setFloat64(0,e!==e?NaN:e),qt(le(qg.getInt32(0),qg.getInt32(4))))},Re=e=>{let t=5381,r=e.length;for(;r;)t=t*33^e.charCodeAt(--r);return qt(t)},Jg=(e,t)=>{let r=12289;for(let n of t)r^=le(H(n),H(e[n]));return qt(r)},Tf=e=>Jg(e,Sf(e)),pC=(e,t)=>r=>{let n=e;for(let o of r)n^=t(o);return qt(n)},To=e=>{let t=6151;for(let r of e)t=le(t,H(r));return qt(t)},tB=pC(Re(\"Map\"),([e,t])=>le(H(e),H(t))),uC=Re(\"Set\"),rB=pC(uC,e=>le(uC,H(e))),Hg=new WeakMap,fC=new WeakMap,Wg=new WeakSet;var ae=\"~effect/Equal\";function J(){return arguments.length===1?e=>Fa(e,arguments[0]):Fa(arguments[0],arguments[1])}function Fa(e,t){if(e===t)return!0;if(e==null||t==null)return!1;let r=typeof e;return r!==typeof t?!1:r===\"number\"&&e!==e&&t!==t?!0:r!==\"object\"&&r!==\"function\"||lc.has(e)||lc.has(t)?!1:nB(e,t)}function nB(e,t){let r=fd.length;for(let s=r;s-- >0;)if(fd[s]===e&&Kg[s]===t)return!0;if(r)return dC(e,t);let n=mC.get(e);n||mC.set(e,n=new WeakMap);let o=n.get(t);return o===void 0&&n.set(t,o=dC(e,t)),o}function dC(e,t){fd.push(e),Kg.push(t);try{return oB(e,t)}finally{fd.pop(),Kg.pop()}}var fd=[],Kg=[],mC=new WeakMap;function oB(e,t){if(H(e)!==H(t))return!1;if(e instanceof Date){if(!(t instanceof Date))return!1;let n=e.getTime(),o=t.getTime();return n===o||Number.isNaN(n)&&Number.isNaN(o)}else if(e instanceof RegExp)return t instanceof RegExp&&e.toString()===t.toString();let r=xC(e);if(r!==xC(t)||typeof e==\"function\"&&!r)return!1;if(r)return e[ae](t);if(Array.isArray(e))return!Array.isArray(t)||e.length!==t.length?!1:sB(e,t);if(ArrayBuffer.isView(e)){let n=e instanceof DataView;return!ArrayBuffer.isView(t)||e.byteLength!==t.byteLength||n!==t instanceof DataView?!1:n?hC(bf(e),bf(t)):hC(e,t)}else{if(e instanceof Map)return!(t instanceof Map)||e.size!==t.size?!1:Gg(e,t,iB,cB);if(e instanceof Set)return!(t instanceof Set)||e.size!==t.size?!1:Gg(e,t,H,Fa)}return aB(e,t)}function sB(e,t){for(let r=0;rH(e[0]),cB=(e,t)=>Fa(e[0],t[0])&&Fa(e[1],t[1]),uB=()=>0;function ld(e,t){return wf((r,n)=>e(r[0],n[0])&&t(r[1],n[1]))}function wf(e){return function(r,n){return Gg(r,n,uB,e)}}var xC=e=>k(e,ae),Vg=()=>J;var gC=e=>(lc.add(e),e);var Na=Symbol.for(\"~effect/Redactable\"),fB=e=>k(e,Na);function is(e){return fB(e)?Zg(e):e}function Zg(e){return e[Na](globalThis[pc]?.context??lB)}var pc=\"~effect/Fiber/currentFiber\",yC=new Map,lB={\"~effect/Context\":{},base:yC,depth:0,mapUnsafe:yC,pipe(){return Z(this,arguments)}};function F(e,t){let r=t?.space??0,n=new WeakSet,o=r?typeof r==\"number\"?\" \".repeat(r):r:\"\",s=f=>o.repeat(f),a=(f,p)=>{let d=f?.constructor;return d&&d!==Object.prototype.constructor&&d.name?`${d.name}(${p})`:p},i=f=>{try{return Reflect.ownKeys(f)}catch{return[\"[ownKeys threw]\"]}};function c(f,p=0){try{return u(f,p)}catch{return(typeof f==\"object\"&&f!==null||typeof f==\"function\")&&n.delete(f),\"[inspection threw]\"}}function u(f,p=0){if(typeof f==\"string\")return JSON.stringify(f);if(typeof f==\"number\"||f==null||typeof f==\"boolean\"||typeof f==\"symbol\")return String(f);if(typeof f==\"bigint\")return String(f)+\"n\";if(typeof f==\"object\"||typeof f==\"function\"){if(n.has(f))return pB;n.add(f);let d;if(Na in f)d=c(Zg(f),p);else if(Array.isArray(f))d=!o||f.length<=1?`[${f.map(m=>c(m,p)).join(\",\")}]`:`[\n${s(p+1)}${f.map(m=>c(m,p+1)).join(`,\n`+s(p+1))}\n${s(p)}]`;else if(f instanceof Date)d=Yg(f);else if(!t?.ignoreToString&&k(f,\"toString\")&&typeof f.toString==\"function\"&&f.toString!==Object.prototype.toString&&f.toString!==Array.prototype.toString){let m=dB(f);d=f instanceof Error&&f.cause!==void 0?`${m} (cause: ${c(f.cause,p)})`:m}else if(Symbol.iterator in f)d=`${f.constructor.name}(${c(Array.from(f),p)})`;else{let m=i(f);if(!o||m.length<=1){let x=`{${m.map(y=>`${wo(y)}:${c(bC(f,y),p)}`).join(\",\")}}`;d=a(f,x)}else{let x=`{\n${m.map(y=>`${s(p+1)}${wo(y)}: ${c(bC(f,y),p+1)}`).join(`,\n`)}\n${s(p)}}`;d=a(f,x)}}return n.delete(f),d}return String(f)}return c(e,0)}var pB=\"[Circular]\";function wo(e){return typeof e==\"string\"?JSON.stringify(e):String(e)}function pd(e){return e.map(t=>`[${wo(t)}]`).join(\"\")}function Yg(e){try{return e.toISOString()}catch{return\"Invalid Date\"}}function dB(e){try{let t=e.toString();return typeof t==\"string\"?t:String(t)}catch{return\"[toString threw]\"}}function bC(e,t){try{return e[t]}catch{return\"[property access threw]\"}}function Ua(e,t){let r=[];return JSON.stringify(e,function(n,o){let s=Object.getOwnPropertyDescriptor(this,n)?.value,a=k(s,Na)?is(s):is(o);if(typeof a==\"bigint\")return F(a);if(typeof a!=\"object\"||a===null)return a;let i=a instanceof Error&&!k(a,\"toJSON\")?{...a,name:a.name,message:a.message}:a;for(;r.length>0&&r[r.length-1]!==this;)r.pop();if(r.includes(a))return;if(r.push(a),i!==a&&r.push(i),!mB(i)||hB(i))return i;let c=new Proxy(i,{get(u,f){let p=Reflect.get(u,f,u);return Object.getOwnPropertyDescriptor(u,f)?.get!==void 0?is(p):p}});return r.push(c),c},t?.space)??\"null\"}function mB(e){for(let t of Object.getOwnPropertyNames(e))if(Object.getOwnPropertyDescriptor(e,t)?.get!==void 0)return!0;return!1}function hB(e){for(let t of[Number.prototype.valueOf,Boolean.prototype.valueOf,String.prototype.valueOf])try{return Reflect.apply(t,e,[]),!0}catch{}return!1}var Te=Symbol.for(\"nodejs.util.inspect.custom\"),vt=e=>{try{return e=is(e),k(e,\"toJSON\")&&Zr(e.toJSON)&&e.toJSON.length===0?e.toJSON():Array.isArray(e)?e.map(vt):e}catch{return\"[toJSON threw]\"}},IC=(e,t=2)=>{if(typeof e==\"string\")return e;try{return typeof e==\"object\"?Ua(e,{space:t}):F(e,{space:t})}catch{return String(e)}},dc={toJSON(){return vt(this)},[Te](){return this.toJSON()},toString(){return F(this.toJSON())}},EC=class{[Te](){return this.toJSON()}toString(){return F(this.toJSON())}};var gB=()=>{let e=Object.getOwnPropertyDescriptor(Error,\"stackTraceLimit\");return e===void 0?Object.isExtensible(Error):Object.hasOwn(e,\"writable\")?e.writable===!0:e.set!==void 0},yB=gB(),Co=()=>Error.stackTraceLimit,Yr=e=>{yB&&(Error.stackTraceLimit=e)};var Bs=class{constructor(t){this.value=t,this.done=!1}next(t){return this.done?(this.value=t,this):(this.done=!0,{value:this.value,done:!1})}},SB=()=>{let e=\"~effect/Utils/internal\",t={[e]:o=>o()},r={[e]:o=>o()};return Co()!==0&&t[e](()=>new Error().stack)?.includes(e)===!0?t[e]:r[e]},js=SB();function M(e,t,r){t===\"__proto__\"?Object.defineProperty(e,t,{value:r,writable:!0,enumerable:!0,configurable:!0}):e[t]=r}function md(e,t){for(let r of Reflect.ownKeys(t))Object.prototype.propertyIsEnumerable.call(t,r)&&M(e,r,t[r])}var ko=\"~effect/Effect\",Da=\"~effect/Exit\",EB={_A:R,_E:R,_R:R},Qg=`${ko}/identifier`,ue=`${ko}/args`,Ke=`${ko}/evaluate`,_t=`${ko}/successCont`,Qr=`${ko}/failureCont`,La=`${ko}/ensureCont`,ja=Symbol.for(\"effect/Effect/Yield\"),rr={pipe(){return Z(this,arguments)},toJSON(){return{...this}},toString(){return F(this.toJSON(),{ignoreToString:!0,space:2})},[Te](){return this.toJSON()}},wC={[se](){return Jg(this,Object.keys(this))},[ae](e){let t=Object.keys(this),r=Object.keys(e);if(t.length!==r.length)return!1;for(let n=0;nk(e,ko),vf=e=>k(e,Da),Cf=\"~effect/Cause\",mc=\"~effect/Cause/Reason\",xc=e=>k(e,Cf),CC=e=>k(e,mc),Ao=class{constructor(t){this[Cf]=Cf,this.reasons=t}pipe(){return Z(this,arguments)}toJSON(){return{_id:\"Cause\",failures:this.reasons.map(t=>t.toJSON())}}toString(){return`Cause(${F(this.reasons)})`}[Te](){return this.toJSON()}[ae](t){return xc(t)&&this.reasons.length===t.reasons.length&&this.reasons.every((r,n)=>J(r,t.reasons[n]))}[se](){return To(this.reasons)}},TC=new WeakMap,hc=class{[mc];annotations;_tag;constructor(t,r,n){if(this[mc]=mc,this._tag=t,r!==Of&&typeof n==\"object\"&&n!==null&&r.size>0){let o=TC.get(n);o&&(r=new Map([...o,...r])),TC.set(n,r)}this.annotations=r}annotate(t,r){if(t.mapUnsafe.size===0)return this;let n=new Map(this.annotations);t.mapUnsafe.forEach((s,a)=>{r?.overwrite!==!0&&n.has(a)||n.set(a,s)});let o=Object.assign(Object.create(Object.getPrototypeOf(this)),this);return o.annotations=n,o}pipe(){return Z(this,arguments)}toString(){return F(this)}[Te](){return this.toString()}},Of=new Map,$a=class extends hc{constructor(t,r=Of){super(\"Fail\",r,t),this.error=t}toString(){return`Fail(${F(this.error)})`}toJSON(){return{_tag:\"Fail\",error:this.error}}[ae](t){return jn(t)&&J(this.error,t.error)&&J(this.annotations,t.annotations)}[se](){return le(Re(this._tag))(le(H(this.error))(H(this.annotations)))}},cs=e=>new Ao(e),TB=(e,t)=>{let r=new Map,n=[];for(let o of e.concat(t)){let s=H(o),a=r.get(s);if(a===void 0)r.set(s,[o]);else{if(a.some(i=>J(i,o)))continue;a.push(o)}n.push(o)}return n},za=l(2,(e,t)=>{if(e.reasons.length===0)return t;if(t.reasons.length===0)return e;let r=new Ao(TB(e.reasons,t.reasons));return J(e,r)?e:r}),hd=new Ao([]),gc=e=>new Ao([new $a(e)]),Af=class extends hc{constructor(t,r=Of){super(\"Die\",r,t),this.defect=t}toString(){return`Die(${F(this.defect)})`}toJSON(){return{_tag:\"Die\",defect:this.defect}}[ae](t){return qa(t)&&J(this.defect,t.defect)&&J(this.annotations,t.annotations)}[se](){return le(Re(this._tag))(le(H(this.defect))(H(this.annotations)))}},Xg=e=>new Ao([new Af(e)]),yc=l(e=>xc(e[0]),(e,t,r)=>t.mapUnsafe.size===0?e:new Ao(e.reasons.map(n=>n.annotate(t,r)))),jn=e=>e._tag===\"Fail\",qa=e=>e._tag===\"Die\",Ha=e=>e._tag===\"Interrupt\";function wB(e){return Er(\"Effect.evaluate: Not implemented\")}var Xr=e=>({...IB,[Qg]:e.op,[Ke]:e[Ke]??wB,[_t]:e[_t],[Qr]:e[Qr],[La]:e[La]}),En=e=>{let t=Xr(e),r=function(n){this[ue]=n};return r.prototype=t,function(n){return new r(n)}},AC=e=>{let t={[Da]:Da,_tag:e.op,get[e.prop](){return this[ue]},...Xr(e),toString(){return`${e.op}(${F(this[ue])})`},toJSON(){return{_id:\"Exit\",_tag:e.op,[e.prop]:this[ue]}},[ae](n){return vf(n)&&n._tag===this._tag&&J(this[ue],n[ue])},[se](){return le(Re(e.op),H(this[ue]))}},r=function(n){this[ue]=n};return r.prototype=t,function(n){return new r(n)}},Ae=AC({op:\"Success\",prop:\"value\",[Ke](e){let t=e.getCont(_t);return t?t[_t](this[ue],e,this):e.yieldWith(this)}}),xd={key:\"effect/Cause/StackTrace\"},ey={key:\"effect/Cause/InterruptorStackTrace\"},Ht=AC({op:\"Failure\",prop:\"cause\",[Ke](e){let t=this[ue],r=!1;e.cache.stackFrame&&(t=yc(t,{mapUnsafe:new Map([[xd.key,e.cache.stackFrame]])}),r=!0);let n=e.getCont(Qr),o=e._interruptedCause;if(o&&e.interruptible){let s=!1;for(;n&&e.interruptible;)s||=Qg in n,n=e.getCont(Qr);s&&(t=cs(t.reasons.filter(a=>a._tag!==\"Fail\"))),t=za(t,o),r=!0}return n?n[Qr](t,e,r?void 0:this):e.yieldWith(r?Ht(t):this)}}),en=e=>Ht(gc(e)),Er=e=>Ht(Xg(e)),re=En({op:\"WithFiber\",[Ke](e){return this[ue](e)}}),kC=En({op:\"WithFiberSucceed\",[Ke](e){let t=this[ue](e),r=e.getCont(_t);return r?r[_t](t,e):e.yieldWith(Ae(t))}}),CB=(function(){class e extends globalThis.Error{}let t=Xr({op:\"YieldableError\",[Ke](){return en(this)}});return delete t.toString,Object.assign(e.prototype,t),e})(),_f=(function(){let e=Symbol.for(\"effect/Data/Error/plainArgs\");return class extends CB{constructor(r){super(r?.message,r?.cause?{cause:r.cause}:void 0),r&&(md(this,r),Object.defineProperty(this,e,{value:r,enumerable:!1}))}toJSON(){return{...this[e],...this}}}})(),us=e=>{class t extends _f{_tag=e}return t.prototype.name=e,t},kf=\"~effect/Cause/NoSuchElementError\",gd=e=>k(e,kf),Bn=class extends us(\"NoSuchElementError\"){[kf]=kf;constructor(t){super({message:t})}},Ba=\"~effect/Cause/Done\",vC=e=>k(e,Ba),OC={[Ba]:Ba,_tag:\"Done\",value:void 0},Wa=e=>e===void 0?OC:{[Ba]:Ba,_tag:\"Done\",value:e},AB=en(OC),_C=e=>e===void 0?AB:en(Wa(e));var yd=e=>Xr({op:e.label,[Ke]:e.evaluate});var xe={};Eo(xe,{Do:()=>fj,all:()=>ZB,andThen:()=>HB,as:()=>JC,asVoid:()=>zB,bind:()=>uj,bindTo:()=>ij,composeK:()=>GB,contains:()=>sj,containsWith:()=>ZC,exists:()=>aj,filter:()=>Va,filterMap:()=>ej,firstSomeOf:()=>BB,flatMap:()=>Ka,flatMapNullishOr:()=>WB,flatten:()=>Ga,fromIterable:()=>FB,fromNullOr:()=>cy,fromNullishOr:()=>Nf,fromUndefinedOr:()=>iy,gen:()=>lj,getFailure:()=>UB,getOrElse:()=>Ff,getOrNull:()=>Rd,getOrThrow:()=>uy,getOrThrowWith:()=>Pd,getOrUndefined:()=>qs,getSuccess:()=>NB,isNone:()=>fe,isOption:()=>wc,isSome:()=>be,let:()=>cj,lift2:()=>nj,liftNullishOr:()=>jB,liftPredicate:()=>oj,liftThrowable:()=>zn,makeCombinerFailFast:()=>YC,makeEquivalence:()=>tj,makeOrder:()=>rj,makeReducer:()=>pj,makeReducerFailFast:()=>dj,map:()=>Rt,match:()=>Wt,none:()=>v,orElse:()=>DB,orElseResult:()=>$B,orElseSome:()=>LB,partitionMap:()=>XB,product:()=>GC,productMany:()=>VB,reduceCompact:()=>YB,some:()=>O,tap:()=>KC,toArray:()=>QB,toRefinement:()=>MB,void:()=>qB,zipLeft:()=>KB,zipRight:()=>JB,zipWith:()=>VC});function RC(e){return{combine:e}}function ry(e,t,r){return{combine:e,initialValue:t,combineAll:r??(n=>{let o=t;for(let s of n)o=e(o,s);return o})}}var nr=e=>(t,r)=>t===r||e(t,r),kB=(e,t)=>e===t,Ja=()=>kB;function FC(e){return nr((t,r)=>{if(t.length!==r.length)return!1;for(let n=0;n{if(t.length!==r.length)return!1;for(let n=0;nl(3,(t,r,n)=>e(t,o=>({...o,[r]:n(o)}))),Ed=e=>l(2,(t,r)=>e(t,n=>({[r]:n}))),Id=(e,t)=>l(3,(r,n,o)=>t(r,s=>e(o(s),a=>({...s,[n]:a}))));var UC=\"~effect/Option\",DC={[UC]:{_A:e=>e},...rr,[Symbol.iterator](){return new Bs(this)}},vB=Object.defineProperty(Object.assign(Object.create(DC),{_tag:\"Some\",_op:\"Some\",[ae](e){return Td(e)&&ny(e)&&J(this.value,e.value)},[se](){return le(H(this._tag))(H(this.value))},toString(){return`some(${F(this.value)})`},toJSON(){return{_id:\"Option\",_tag:this._tag,value:vt(this.value)}}}),\"valueOrUndefined\",{get(){return this.value}}),OB=H(\"None\"),_B=Object.assign(Object.create(DC),{_tag:\"None\",_op:\"None\",valueOrUndefined:void 0,[ae](e){return Td(e)&&wd(e)},[se](){return OB},toString(){return\"none()\"},toJSON(){return{_id:\"Option\",_tag:this._tag}}}),Td=e=>k(e,UC),wd=e=>e._tag===\"None\",ny=e=>e._tag===\"Some\",Rf=Object.create(_B),LC=function(e){this.value=e};LC.prototype=vB;var Pf=e=>new LC(e);var BC=\"~effect/Result\",jC={[BC]:{_A:e=>e,_E:e=>e},...rr,[Symbol.iterator](){return new Bs(this)}},RB=Object.assign(Object.create(jC),{_tag:\"Success\",_op:\"Success\",[ae](e){return Cd(e)&&bc(e)&&J(this.success,e.success)},[se](){return le(H(this._tag))(H(this.success))},toString(){return`success(${F(this.success)})`},toJSON(){return{_id:\"Result\",_tag:this._tag,value:vt(this.success)}}}),PB=Object.assign(Object.create(jC),{_tag:\"Failure\",_op:\"Failure\",[ae](e){return Cd(e)&&Sc(e)&&J(this.failure,e.failure)},[se](){return le(H(this._tag))(H(this.failure))},toString(){return`failure(${F(this.failure)})`},toJSON(){return{_id:\"Result\",_tag:this._tag,failure:vt(this.failure)}}}),Cd=e=>k(e,BC),Sc=e=>e._tag===\"Failure\",bc=e=>e._tag===\"Success\",zC=function(e){this.failure=e};zC.prototype=PB;var Ad=e=>new zC(e),qC=function(e){this.success=e};qC.prototype=RB;var kd=e=>new qC(e),oy=e=>bc(e)?Rf:Pf(e.failure),sy=e=>Sc(e)?Rf:Pf(e.success);function zs(e){return(t,r)=>t===r?0:e(t,r)}var WC=zs((e,t)=>eglobalThis.Number.isNaN(e)&&globalThis.Number.isNaN(t)?0:globalThis.Number.isNaN(e)?-1:globalThis.Number.isNaN(t)?1:eezs((r,n)=>e(t(r),t(n)))),Ic=ay(fs,e=>e.getTime());var vd=e=>l(2,(t,r)=>e(t,r)===-1),Tc=e=>l(2,(t,r)=>e(t,r)===1),Od=e=>l(2,(t,r)=>e(t,r)!==1),_d=e=>l(2,(t,r)=>e(t,r)!==-1);var v=()=>Rf,O=Pf,wc=Td,fe=wd,be=ny,Wt=l(2,(e,{onNone:t,onSome:r})=>fe(e)?t():r(e.value)),MB=e=>t=>be(e(t)),FB=e=>{for(let t of e)return O(t);return v()},NB=sy,UB=oy,Ff=l(2,(e,t)=>fe(e)?t():e.value),DB=l(2,(e,t)=>fe(e)?t():e),LB=l(2,(e,t)=>fe(e)?O(t()):e),$B=l(2,(e,t)=>fe(e)?Rt(t(),kd):Rt(e,Ad)),BB=e=>{let t=v();for(t of e)if(be(t))return t;return t},Nf=e=>e==null?v():O(e),iy=e=>e===void 0?v():O(e),cy=e=>e===null?v():O(e),jB=e=>(...t)=>Nf(e(...t)),Rd=Ff($g),qs=Ff(bn),zn=e=>(...t)=>{try{return O(e(...t))}catch{return v()}},Pd=l(2,(e,t)=>{if(be(e))return e.value;throw t()}),uy=Pd(()=>new Error(\"getOrThrow called on a None\")),Rt=l(2,(e,t)=>fe(e)?v():O(t(e.value))),JC=l(2,(e,t)=>Rt(e,()=>t)),zB=JC(void 0),qB=O(void 0);var Ka=l(2,(e,t)=>fe(e)?v():t(e.value)),HB=l(2,(e,t)=>Ka(e,r=>{let n=Zr(t)?t(r):t;return wc(n)?n:O(n)})),WB=l(2,(e,t)=>fe(e)?v():Nf(t(e.value))),Ga=Ka(R),JB=l(2,(e,t)=>Ka(e,()=>t)),KB=l(2,(e,t)=>KC(e,()=>t)),GB=l(2,(e,t)=>r=>Ka(e(r),t)),KC=l(2,(e,t)=>Ka(e,r=>Rt(t(r),()=>r))),GC=l(2,(e,t)=>be(e)&&be(t)?O([e.value,t.value]):v()),VB=l(2,(e,t)=>{if(fe(e))return v();let r=[e.value];for(let n of t){if(fe(n))return v();r.push(n.value)}return O(r)}),ZB=e=>{if(Symbol.iterator in e){let r=[];for(let n of e){if(fe(n))return v();r.push(n.value)}return O(r)}let t={};for(let r of Object.keys(e)){let n=e[r];if(fe(n))return v();M(t,r,n.value)}return O(t)},VC=l(3,(e,t,r)=>Rt(GC(e,t),([n,o])=>r(n,o))),YB=l(3,(e,t,r)=>{let n=t;for(let o of e)be(o)&&(n=r(n,o.value));return n}),QB=e=>fe(e)?[]:[e.value],XB=l(2,(e,t)=>{if(fe(e))return[v(),v()];let r=t(e.value);return Sc(r)?[v(),O(r.failure)]:[O(r.success),v()]}),ej=l(2,(e,t)=>{if(fe(e))return v();let r=t(e.value);return bc(r)?O(r.success):v()}),Va=l(2,(e,t)=>fe(e)?v():t(e.value)?O(e.value):v()),tj=e=>nr((t,r)=>fe(t)?fe(r):fe(r)?!1:e(t.value,r.value)),rj=e=>zs((t,r)=>be(t)?be(r)?e(t.value,r.value):1:-1),nj=e=>l(2,(t,r)=>VC(t,r,e)),oj=l(2,(e,t)=>t(e)?O(e):v()),ZC=e=>l(2,(t,r)=>fe(t)?!1:e(t.value,r)),sj=ZC(Vg()),aj=l(2,(e,t)=>fe(e)?!1:t(e.value)),ij=Ed(Rt),cj=bd(Rt);var uj=Id(Rt,Ka),fj=O({}),lj=(...e)=>{let r=(e.length===1?e[0]:e[1].bind(e[0]))(),n=r.next();for(;!n.done;){let o=n.value;if(fe(o))return o;n=r.next(o.value)}return O(n.value)};function pj(e){return ry((t,r)=>fe(t)?r:fe(r)?t:O(e.combine(t.value,r.value)),v())}function YC(e){return RC((t,r)=>fe(t)||fe(r)?v():O(e.combine(t.value,r.value)))}function dj(e){let t=YC(e).combine,r=O(e.initialValue);return ry(t,r,n=>{let o=r;for(let s of n)if(o=t(o,s),fe(o))return o;return o})}var QC=\"~effect/Context/Service\",lr=function(){function e(){}let t=e;Object.setPrototypeOf(t,mj);let r=(n,o)=>(t.key=n,o?.defaultValue&&(t[ly]=ly,t.defaultValue=o.defaultValue),o?.make&&(t.make=o.make),o?.fiberCached&&XC.add(n),t);return arguments.length>0?r(arguments[0],arguments[1]):r},mj={[QC]:QC,...yd({label:\"Service\",evaluate(e){return Ae(Fe(e.context,this))}}),toJSON(){return{_id:\"Service\",key:this.key}},of(e){return e},context(e){return Za(this,e)},use(e){return re(t=>e(Fe(t.context,this)))},useSync(e){return re(t=>Ae(e(Fe(t.context,this))))}},XC=new Set,ly=\"~effect/Context/Reference\",eA=\"~effect/Context\",hj=8,xj=8,py=(e,t,r,n)=>{let o=Object.create(yj);return o.cacheRoot=e??o,o.base=t,o.overlay=r,o.depth=n,o._flat=void 0,o.baseHits=0,o},tA=(e,t)=>{t&&(tA(e,t.parent),e.set(t.key,t.value))},rA=e=>{if(e._flat)return e._flat;if(!e.overlay)return e._flat=e.base;let t=new Map(e.base);return tA(t,e.overlay),e._flat=t},gj=(e,t)=>{let r=new Map(e.mapUnsafe);return t(r),Oo(r)},Md=Symbol(),dy=(e,t)=>{let r=e;for(let o=r.overlay;o;o=o.parent)if(o.key===t)return o.value;let n=r.base.get(t);return n===void 0&&!r.base.has(t)?Md:(r.overlay&&++r.baseHits>=r.base.size&&r.baseHits>=xj&&(r.base=rA(r),r.overlay=void 0,r.depth=0),n)},Oo=e=>py(void 0,e,void 0,0),yj={get mapUnsafe(){return rA(this)},...rr,[eA]:{_Services:e=>e},toJSON(){return{_id:\"Context\",services:Array.from(this.mapUnsafe).map(([e,t])=>({key:e,value:t}))}},[ae](e){if(!Uf(e))return!1;let t=this.mapUnsafe,r=e.mapUnsafe;if(t.size!==r.size)return!1;for(let[n,o]of t)if(!r.has(n)||!J(o,r.get(n)))return!1;return!0},[se](){return It(this.mapUnsafe.size)}},nA=(e,t)=>e.cacheRoot===t.cacheRoot,Uf=e=>k(e,eA);var oA=e=>!!e[ly],Ir=()=>Sj,Sj=Oo(new Map),Za=(e,t)=>Oo(new Map([[e.key,t]])),Jt=l(3,(e,t,r)=>bj(e,t.key,r)),bj=(e,t,r)=>{let n=e,o=XC.has(t)?void 0:n.cacheRoot;if(n.depth>=hj){let s=new Map(n.mapUnsafe);return s.set(t,r),py(o,s,void 0,0)}return py(o,n.base,{key:t,value:r,parent:n.overlay},n.depth+1)};var Fd=l(2,(e,t)=>Df(e,t.key)),Df=(e,t)=>{let r=dy(e,t);return r===Md?void 0:r},_o=l(2,(e,t)=>{let r=dy(e,t.key);if(r===Md){if(oA(t))return sA(t);throw Ej(t)}return r}),Fe=_o,fy=\"~effect/Context/defaultValue\",sA=e=>fy in e?e[fy]:e[fy]=e.defaultValue(),Ej=e=>{let t=new Error(`Service not found${e.key?`: ${String(e.key)}`:\"\"}`);if(t.stack){let r=t.stack.split(`\n`);r.splice(1,3),t.stack=r.join(`\n`)}return t},Nd=l(2,(e,t)=>{let r=dy(e,t.key);return r!==Md?O(r):oA(t)?O(sA(t)):v()}),Ud=l(2,(e,t)=>e.mapUnsafe.size===0?t:t.mapUnsafe.size===0?e:gj(e,r=>t.mapUnsafe.forEach((n,o)=>r.set(o,n)))),aA=(...e)=>{let t=new Map;for(let r=0;r{t.set(o,n)});return Oo(t)};var we=lr;var my=e=>e.length>0;var ot=e=>e>0?Math.floor(e):0,Tn=e=>Math.max(1,ot(e));var B=kd,W=Ad;var iA=Cd,ce=Sc,mt=bc;var Dd=l(2,(e,t)=>ce(e)?W(t(e.failure)):e),Tr=l(2,(e,t)=>mt(e)?B(t(e.success)):e),wr=l(2,(e,{onFailure:t,onSuccess:r})=>ce(e)?t(e.failure):r(e.success));var Lf=l(2,(e,t)=>ce(e)?t(e.failure):e.success);var Tj=l(2,(e,t)=>{if(mt(e))return e.success;throw t(e.failure)}),$f=Tj(R);var Lt=l(2,(e,t)=>ce(e)?W(e.failure):t(e.success));var hy=e=>{if(Symbol.iterator in e){let r=[];for(let n of e){if(ce(n))return n;r.push(n.success)}return B(r)}let t={};for(let r of Object.keys(e)){let n=e[r];if(ce(n))return n;M(t,r,n.success)}return B(t)};var cA=FC;var wj=(e,t)=>{let r=t?.length!==void 0?Tn(t.length):1/0;return{[Symbol.iterator](){let n=0;return{next(){return nAj(wj(()=>e,{length:t}))),fA=e=>Cj(e,1/0);var lA=e=>{let r=e[Symbol.iterator]().next();if(r.done)throw new Error(\"headUnsafe: empty iterable\");return r.value};var pA=l(2,(e,t)=>{let r=0;for(let n of e){let o=t(n,r);if(ad(o)){if(o)return O(n)}else if(be(o))return o;r++}return v()});var Aj=e=>({[Symbol.iterator](){let t=e[Symbol.iterator](),r;function n(){for(;;){if(r===void 0){let s=t.next();if(s.done)return s;r=s.value[Symbol.iterator]()}let o=r.next();if(!o.done)return o;r=void 0}}return{next:n}}});var dA=l(2,(e,t)=>({[Symbol.iterator](){let r=e[Symbol.iterator](),n=0;return{next(){let o=r.next();for(;!o.done;){if(t(o.value,n++))return{done:!1,value:o.value};o=r.next()}return{done:!0,value:void 0}}}}}));var kj=l(2,(e,t)=>Object.hasOwn(e,t));var Ya=l(2,(e,t)=>{let r={...e};for(let n of mA(e))M(r,n,t(e[n],n));return r});var mA=e=>Object.keys(e);var vj=e=>l(2,(t,r)=>{for(let n of mA(t))if(!kj(r,n)||!e(t[n],r[n]))return!1;return!0});var hA=e=>{let t=vj(e);return(r,n)=>t(r,n)&&t(n,r)};var Ld=globalThis.Array;var xA=l(2,(e,t)=>{let r=Tn(e),n=new Ld(r);for(let o=0;oe<=t?xA(t-e+1,r=>e+r):[e];var Ye=e=>Ld.isArray(e)?e:Ld.from(e);var $d=l(2,(e,t)=>[...e,t]),gy=l(2,(e,t)=>Ye(e).concat(Ye(t)));var gA=Ld.isArray;var or=my,tt=my;function Cc(e){let t=Number(e);return String(t)===e&&Number.isInteger(t)&&t>=0&&t<2**32-1}function Oj(e,t){return!Number.isFinite(e)||e<0||e>=t.length}var yA=l(2,(e,t)=>{let r=Math.floor(t);if(Oj(r,e))throw new Error(`Index out of bounds: ${r}`);return e[r]});var Bd=e=>e[e.length-1];var yy=pA;var _j=(e,t)=>{let r=H(t),n=e.get(r);if(n===void 0)return e.set(r,[t]),!0;for(let o of n)if(J(o,t))return!1;return n.push(t),!0};var Pt=()=>[],Ee=e=>[e],wn=l(2,(e,t)=>e.map(t));var jd=l(2,(e,t)=>{let r=Ye(e),n=[];for(let o=0;o{let r=[],n=[],o=0;for(let s of e){let a=t(s,o++);mt(a)?r.push(a.success):n.push(a.failure)}return[r,n]});var SA=Sd;var zd=e=>{let t=Ye(e);if(t.length<2)return[...t];let r=new Map,n=[];for(let o of t)_j(r,o)&&n.push(o);return n};var Hd=\"~effect/Duration\",kc=BigInt(0),CA=BigInt(1),Rj=BigInt(2),Pj=BigInt(10),bA=BigInt(24),qd=BigInt(60),by=BigInt(1e3),EA=BigInt(1e6);var Wd=e=>BigInt(e<0?Math.ceil(e-.5):Math.floor(e+.5)),AA=e=>Wd(e*1e6),IA=(e,t)=>{let r=e.indexOf(\".\");if(r===-1)return BigInt(e)*t;let n=e[0]===\"-\",o=e.slice(r+1),s=Pj**BigInt(o.length),a=(BigInt(e.slice(n?1:0,r))*s+BigInt(o))*t,i=a/s+(a%s*Rj>=s?CA:kc);return n?-i:i};var Mj=/^(-?\\d+(?:\\.\\d+)?)\\s+(nanos?|micros?|millis?|seconds?|minutes?|hours?|days?|weeks?)$/,Ge=e=>{switch(typeof e){case\"number\":return Js(e);case\"bigint\":return Ro(e);case\"string\":{if(e===\"Infinity\")return ls;if(e===\"-Infinity\")return Ac;let t=Mj.exec(e);if(!t)break;let[r,n,o]=t;if(o===\"nano\"||o===\"nanos\")return Ro(IA(n,CA));if(o===\"micro\"||o===\"micros\")return Ro(IA(n,by));let s=Number(n);switch(o){case\"milli\":case\"millis\":return Js(s);case\"second\":case\"seconds\":return Iy(s);case\"minute\":case\"minutes\":return $j(s);case\"hour\":case\"hours\":return Bj(s);case\"day\":case\"days\":return jj(s);case\"week\":case\"weeks\":return zj(s)}break}case\"object\":{if(e===null)break;if(Hd in e)return e;if(Array.isArray(e))return e.length!==2||!e.every(Ef)?TA(e):Number.isNaN(e[0])||Number.isNaN(e[1])?Ws:e[0]===-1/0||e[1]===-1/0?Ac:e[0]===1/0||e[1]===1/0?ls:ar(Wd(e[0]*1e9+e[1]));let t=e,r=0;return t.weeks&&(r+=t.weeks*6048e5),t.days&&(r+=t.days*864e5),t.hours&&(r+=t.hours*36e5),t.minutes&&(r+=t.minutes*6e4),t.seconds&&(r+=t.seconds*1e3),t.milliseconds&&(r+=t.milliseconds),!t.microseconds&&!t.nanoseconds?ar(r):ar(Wd(r*1e6+(t.microseconds??0)*1e3+(t.nanoseconds??0)))}}return TA(e)},TA=e=>{throw new Error(`Invalid Input: ${e}`)},kA=zn(Ge),wA={_tag:\"Millis\",millis:0},Fj={_tag:\"Infinity\"},Nj={_tag:\"NegativeInfinity\"},Uj={[Hd]:Hd,[se](){switch(this.value._tag){case\"Millis\":{let e=this.value.millis*1e6;return Number.isFinite(e)?H(Wd(e)):It(this.value.millis)}case\"Nanos\":return H(this.value.nanos);default:return Tf(this.value)}},[ae](e){return Ey(e)&&Hj(this,e)},toString(){switch(this.value._tag){case\"Infinity\":return\"Infinity\";case\"NegativeInfinity\":return\"-Infinity\";case\"Nanos\":return`${this.value.nanos} nanos`;case\"Millis\":return`${this.value.millis} millis`}},toJSON(){switch(this.value._tag){case\"Millis\":return{_id:\"Duration\",_tag:\"Millis\",millis:this.value.millis};case\"Nanos\":return{_id:\"Duration\",_tag:\"Nanos\",nanos:String(this.value.nanos)};case\"Infinity\":return{_id:\"Duration\",_tag:\"Infinity\"};case\"NegativeInfinity\":return{_id:\"Duration\",_tag:\"NegativeInfinity\"}}},[Te](){return this.toJSON()},pipe(){return Z(this,arguments)}},ar=e=>{let t=Object.create(Uj);return typeof e==\"number\"?isNaN(e)||e===0||Object.is(e,-0)?t.value=wA:Number.isFinite(e)?Number.isInteger(e)?t.value={_tag:\"Millis\",millis:e}:t.value={_tag:\"Nanos\",nanos:AA(e)}:t.value=e>0?Fj:Nj:e===kc?t.value=wA:t.value={_tag:\"Nanos\",nanos:e},t},Ey=e=>k(e,Hd),Ks=e=>e.value._tag!==\"Infinity\"&&e.value._tag!==\"NegativeInfinity\",vc=e=>{switch(e.value._tag){case\"Millis\":return e.value.millis===0;case\"Nanos\":return e.value.nanos===kc;case\"Infinity\":case\"NegativeInfinity\":return!1}},Dj=e=>{switch(e.value._tag){case\"Millis\":return e.value.millis<0;case\"Nanos\":return e.value.nanos{switch(e.value._tag){case\"Infinity\":case\"NegativeInfinity\":return ls;case\"Millis\":return e.value.millis<0?ar(-e.value.millis):e;case\"Nanos\":return e.value.nanosar(e);var Js=e=>ar(e),Iy=e=>ar(e*1e3),$j=e=>ar(e*6e4),Bj=e=>ar(e*36e5),jj=e=>ar(e*864e5),zj=e=>ar(e*6048e5),Cr=e=>_A(Ge(e),{onMillis:R,onNanos:t=>Number(t)/1e6,onInfinity:()=>1/0,onNegativeInfinity:()=>-1/0}),vA=e=>_A(Ge(e),{onMillis:t=>t/1e3,onNanos:t=>Number(t)/1e9,onInfinity:()=>1/0,onNegativeInfinity:()=>-1/0});var Jd=e=>{let t=Ge(e);switch(t.value._tag){case\"Infinity\":case\"NegativeInfinity\":throw new Error(\"Cannot convert infinite duration to nanos\");case\"Nanos\":return t.value.nanos;case\"Millis\":return AA(t.value.millis)}},OA=zn(Jd);var _A=l(2,(e,t)=>{switch(e.value._tag){case\"Millis\":return t.onMillis(e.value.millis);case\"Nanos\":return t.onNanos(e.value.nanos);case\"Infinity\":return t.onInfinity();case\"NegativeInfinity\":return(t.onNegativeInfinity??t.onInfinity)()}}),RA=l(3,(e,t,r)=>e.value._tag===\"Infinity\"||e.value._tag===\"NegativeInfinity\"||t.value._tag===\"Infinity\"||t.value._tag===\"NegativeInfinity\"?r.onInfinity(e,t):e.value._tag===\"Millis\"?t.value._tag===\"Millis\"?r.onMillis(e.value.millis,t.value.millis):r.onNanos(Jd(e),t.value.nanos):r.onNanos(e.value.nanos,Jd(t)));var qj=(e,t)=>RA(e,t,{onMillis:(r,n)=>r===n,onNanos:(r,n)=>r===n,onInfinity:(r,n)=>r.value._tag===n.value._tag});var PA=l(2,(e,t)=>RA(e,t,{onMillis:(r,n)=>ar(r-n),onNanos:(r,n)=>ar(r-n),onInfinity:(r,n)=>{let o=r.value._tag,s=n.value._tag;return o===\"Infinity\"?s===\"Infinity\"?Ws:ls:o===\"NegativeInfinity\"?s===\"NegativeInfinity\"?Ws:Ac:s===\"Infinity\"?Ac:ls}}));var Hj=l(2,(e,t)=>qj(e,t)),Wj=e=>{if(e.value._tag===\"Infinity\")return{days:1/0,hours:1/0,minutes:1/0,seconds:1/0,millis:1/0,nanos:1/0};if(e.value._tag===\"NegativeInfinity\")return{days:-1/0,hours:-1/0,minutes:-1/0,seconds:-1/0,millis:-1/0,nanos:-1/0};let t=Jd(e),r=t{if(e.value._tag===\"Infinity\")return\"Infinity\";if(e.value._tag===\"NegativeInfinity\")return\"-Infinity\";if(vc(e))return\"0\";if(Dj(e))return\"-\"+Ty(Lj(e));let t=Wj(e),r=[];return t.days!==0&&r.push(`${t.days}d`),t.hours!==0&&r.push(`${t.hours}h`),t.minutes!==0&&r.push(`${t.minutes}m`),t.seconds!==0&&r.push(`${t.seconds}s`),t.millis!==0&&r.push(`${t.millis}ms`),t.nanos!==0&&r.push(`${t.nanos}ns`),r.join(\" \")};var wy=l(2,(e,t)=>r=>{let n=e(r);if(ce(n))return W(r);let o=t(n.success);return ce(o)?W(r):o}),MA=e=>t=>{let r=e(t);return ce(r)?v():O(r.success)};var zf=we(\"effect/Scheduler\",{fiberCached:!0,defaultValue:()=>new Qa}),NA=e=>{let t=!1;return Promise.resolve().then(()=>{t||e()}),()=>{t=!0}},Jj=\"setImmediate\"in globalThis?e=>{let t=globalThis.setImmediate(e);return()=>globalThis.clearImmediate(t)}:e=>{let t=setTimeout(e,0);return()=>clearTimeout(t)},UA=e=>{try{return Jj(e)}catch{return NA(e)}},Cy=class{buckets=[];scheduleTask(t,r){let n=this.buckets,o=n.length,s,a=0;for(;ar);a++)s=n[a];s&&s[0]===r?s[1].push(t):a===o?n.push([r,[t]]):n.splice(a,0,[r,[t]])}drain(){let t=this.buckets;return this.buckets=[],t}},Qa=class{executionMode;setImmediate;constructor(t=\"async\",r){this.executionMode=t,this.setImmediate=r??(t===\"sync\"?NA:UA)}shouldYield(t){return t.currentOpCount>=t.cache.maxOpsBeforeYield}makeDispatcher(){return new Ay(this.setImmediate)}},Ay=class{tasks=new Cy;running=void 0;setImmediate;constructor(t=UA){this.setImmediate=t}scheduleTask(t,r){this.tasks.scheduleTask(t,r),this.running===void 0&&(this.running=this.setImmediate(this.afterScheduled))}afterScheduled=()=>{this.running=void 0,this.runTasks()};runTasks(){let t=this.tasks.drain();for(let r=0;r0;)this.running!==void 0&&(this.running(),this.running=void 0),this.runTasks()}},DA=we(\"effect/Scheduler/MaxOpsBeforeYield\",{fiberCached:!0,defaultValue:()=>2048}),LA=we(\"effect/Scheduler/PreventSchedulerYield\",{fiberCached:!0,defaultValue:()=>!1});var Cn={};Eo(Cn,{Class:()=>Kd,Error:()=>Gd,TaggedClass:()=>Kj,TaggedError:()=>Fr,taggedEnum:()=>Gj});var Kd=class extends $s{constructor(e){super(),e&&md(this,e)}},Kj=e=>class extends Kd{_tag=e},Gj=()=>new Proxy({},{get(e,t,r){return t===\"$is\"?At:t===\"$match\"?Vj:n=>({...n,_tag:t})}});function Vj(){if(arguments.length===1){let r=arguments[0];return function(n){return r[n._tag](n)}}let e=arguments[0];return arguments[1][e._tag](e)}var Gd=_f,Fr=us;var $A=\"~effect/encoding/EncodingError\",An=class extends Fr(\"EncodingError\"){[$A]=$A};var zA=e=>BA(typeof e==\"string\"?Xj.encode(e):e),vy=e=>{switch(e){case 16:return Yj();case 32:return Qj();default:{let t=\"\";for(let r=e>>>3;r>0;r--)t+=Zj();return t}}},te=Uint8Array.from(\"0123456789abcdef\",e=>e.charCodeAt(0)),Xa=()=>Math.random()*4294967296>>>0,Zj=()=>{let e=Xa();return String.fromCharCode(te[e>>>28],te[e>>>24&15],te[e>>>20&15],te[e>>>16&15],te[e>>>12&15],te[e>>>8&15],te[e>>>4&15],te[e&15])},Yj=()=>{let e=Xa(),t=Xa();return String.fromCharCode(te[e>>>28],te[e>>>24&15],te[e>>>20&15],te[e>>>16&15],te[e>>>12&15],te[e>>>8&15],te[e>>>4&15],te[e&15],te[t>>>28],te[t>>>24&15],te[t>>>20&15],te[t>>>16&15],te[t>>>12&15],te[t>>>8&15],te[t>>>4&15],te[t&15])},Qj=()=>{let e=Xa(),t=Xa(),r=Xa(),n=Xa();return String.fromCharCode(te[e>>>28],te[e>>>24&15],te[e>>>20&15],te[e>>>16&15],te[e>>>12&15],te[e>>>8&15],te[e>>>4&15],te[e&15],te[t>>>28],te[t>>>24&15],te[t>>>20&15],te[t>>>16&15],te[t>>>12&15],te[t>>>8&15],te[t>>>4&15],te[t&15],te[r>>>28],te[r>>>24&15],te[r>>>20&15],te[r>>>16&15],te[r>>>12&15],te[r>>>8&15],te[r>>>4&15],te[r&15],te[n>>>28],te[n>>>24&15],te[n>>>20&15],te[n>>>16&15],te[n>>>12&15],te[n>>>8&15],te[n>>>4&15],te[n&15])},Oy=e=>{let t=new TextEncoder().encode(e);if(t.length%2!==0)return W(new An({module:\"Hex\",kind:\"Decode\",input:e,message:`Length must be a multiple of 2, but is ${t.length}`}));try{let r=t.length/2,n=new Uint8Array(r);for(let o=0;oTr(Oy(e),t=>ez.decode(t)),Xj=new TextEncoder,ez=new TextDecoder,tz=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,\"0\")),BA=e=>{let t=\"\";for(let r=0;r{if(48<=e&&e<=57)return e-48;if(97<=e&&e<=102)return e-97+10;if(65<=e&&e<=70)return e-65+10;throw new TypeError(\"Invalid input\")};var Ry=\"effect/Tracer/ParentSpan\",ei=class extends lr()(Ry,{fiberCached:!0}){},rz=e=>e;var Vd=we(\"effect/Tracer/DisablePropagation\",{defaultValue:yf}),WA=we(\"effect/Tracer/CurrentTraceLevel\",{defaultValue:()=>\"Info\"}),JA=we(\"effect/Tracer/MinimumTraceLevel\",{defaultValue:()=>\"All\"}),Py=\"effect/Tracer\",My=we(Py,{fiberCached:!0,defaultValue:()=>Fy}),Fy=rz({span:e=>new _y(e)}),_y=class{_tag=\"Span\";sampled;name;parent;annotations;links;startTime;kind;status;_traceId=void 0;_spanId=void 0;_attributes=void 0;_events=void 0;constructor(t){this.name=t.name,this.parent=t.parent,this.annotations=t.annotations,this.links=t.links,this.startTime=t.startTime,this.kind=t.kind,this.sampled=t.sampled,this.status={_tag:\"Started\",startTime:t.startTime}}get traceId(){return this._traceId??=qs(this.parent)?.traceId??vy(32)}get spanId(){return this._spanId??=vy(16)}get attributes(){return this._attributes??=new Map}get events(){return this._events??=[]}end(t,r){this.status={_tag:\"Ended\",endTime:t,exit:r,startTime:this.status.startTime}}attribute(t,r){this.attributes.set(t,r)}event(t,r,n){this.events.push([t,r,n??{}])}addLinks(t){this.links.push(...t)}};var GA=\"effect/Metric/FiberRuntimeMetrics\";var VA=we(\"effect/ErrorReporter/CurrentErrorReporters\",{defaultValue:()=>new Set}),qf=we(\"effect/References/CurrentStackFrame\",{fiberCached:!0,defaultValue:bn}),Zd=we(\"effect/References/TracerEnabled\",{fiberCached:!0,defaultValue:Sr}),ti=we(\"effect/References/TracerTimingEnabled\",{defaultValue:Sr}),Hf=we(\"effect/References/TracerSpanAnnotations\",{defaultValue:()=>({})}),Wf=we(\"effect/References/TracerSpanLinks\",{defaultValue:()=>[]}),ps=we(\"effect/References/CurrentLogAnnotations\",{defaultValue:()=>({})}),Ny=we(\"effect/References/CurrentLogLevel\",{fiberCached:!0,defaultValue:()=>\"Info\"}),Uy=we(\"effect/References/MinimumLogLevel\",{fiberCached:!0,defaultValue:()=>\"Info\"});var Yd=we(\"effect/References/CurrentLogSpans\",{defaultValue:()=>[]});var Gs=e=>{if(e?.captureStackTrace===!1)return e;if(e?.captureStackTrace!==void 0&&typeof e.captureStackTrace!=\"boolean\")return e;let t=Co();if(t===0&&e?.captureStackTrace!==!0)return{...e,captureStackTrace:!1};Yr(3);let r=new Error;return Yr(t),{...e,captureStackTrace:sz(()=>r.stack)}},Dy=e=>t=>{let r;return()=>{if(r!==void 0)return r;let n=t();if(!n)return;let o=n.split(`\n`);if(o[e]!==void 0)return r=o[e].trim(),r}},sz=Dy(3);var Qd=class extends hc{constructor(t,r=Of){super(\"Interrupt\",r,\"Interrupted\"),this.fiberId=t}toString(){return`Interrupt(${this.fiberId})`}toJSON(){return{_tag:\"Interrupt\",fiberId:this.fiberId}}[ae](t){return Ha(t)&&this.fiberId===t.fiberId&&this.annotations===t.annotations}[se](){return le(Re(`${this._tag}:${this.fiberId}`))(ud(this.annotations))}},nk=e=>new Qd(e),Zs=e=>new Ao([new Qd(e)]),Gy=e=>e.reasons.some(jn),Vy=e=>{let t=e.reasons.find(jn);return t?B(t):W(e)},Ur=e=>{for(let t=0;te.reasons.some(qa),sk=e=>{let t=e.reasons.find(qa);return t?B(t):W(e)},tl=e=>{let t=e.reasons.find(qa);return t?B(t.defect):W(e)},am=e=>e.reasons.some(Ha),ak=e=>{let t=e.reasons.find(Ha);return t?B(t):W(e)},im=e=>{let t;for(let r=0;r{let t=im(e);return ce(t)?az:t.success},az=new Set,cm=e=>e.reasons.length>0&&e.reasons.every(Ha),ck=e=>Oo(e.annotations),uk=e=>{let t=new Map;for(let r of e.reasons)if(r.annotations.size>0)for(let[n,o]of r.annotations)t.set(n,o);return Oo(t)},fk=l(2,(e,t)=>{let r=!1,n=e.reasons.map(o=>jn(o)?(r=!0,new $a(t(o.error),o.annotations)):o);return r?cs(n):e}),um=e=>{let t;for(let r of e.reasons){if(r._tag===\"Fail\")return r.error;r._tag===\"Die\"&&(t??=r)}return t!==void 0?t.defect:e.reasons.length>0?new globalThis.Error(\"All fibers interrupted without error\"):new globalThis.Error(\"Empty cause\")},Zy=(e,t)=>{let r=[],n=[];if(e.reasons.length===0)return r;let o=Co();o!==0&&Yr(1);try{for(let s of e.reasons){if(s._tag===\"Interrupt\"){n.push(s);continue}r.push($y(s._tag===\"Die\"?s.defect:s.error,s.annotations,t))}if(r.length===0){let s=new Error(\"The fiber was interrupted by:\");s.name=\"InterruptCause\",s.stack=fz(s,n);let a=new globalThis.Error(\"All fibers interrupted without error\",{cause:s});a.name=\"InterruptError\",a.stack=`${a.name}: ${a.message}`,r.push($y(a,n[0].annotations,t))}}finally{o!==0&&Yr(o)}return r},$y=(e,t,r)=>{let n=typeof e,o;if(e&&n===\"object\"){if(o=new globalThis.Error(iz(e),{cause:e.cause?$y(e.cause):void 0}),typeof e.name==\"string\"&&(o.name=e.name),typeof e.stack==\"string\")o.stack=uz(e.stack,o,t);else{let s=`${o.name}: ${o.message}`;o.stack=t?By(s,t):s}r?.includeCauseInStack&&(o.stack=pk(o));for(let s of Object.keys(e))s in o||(o[s]=e[s])}else{o=new globalThis.Error(e?n===\"string\"?e:Ua(e):`Unknown error: ${e}`);let s=`${o.name}: ${o.message}`;o.stack=t?By(s,t):s}return o},iz=e=>{if(typeof e.message==\"string\")return e.message;if(typeof e.toString==\"function\"&&e.toString!==Object.prototype.toString&&e.toString!==Array.prototype.toString)try{return e.toString()}catch{}return Ua(e)},cz=/\\((.*)\\)/g,uz=(e,t,r)=>{let n=`${t.name}: ${t.message}`,o=(e.startsWith(n)?e.slice(n.length):e).split(`\n`),s=[n];for(let a=1;a{let r=t?.get(xd.key);return r&&(e=`${e}\n${lk(r)}`),e},fz=(e,t)=>{let r=[`${e.name}: ${e.message}`];for(let n of t){let o=n.fiberId!==void 0?`#${n.fiberId}`:\"unknown\",s=n.annotations.get(ey.key);r.push(` at fiber (${o})`),s&&r.push(lk(s))}return r.join(`\n`)},lk=e=>{let t=[],r=e,n=0;for(;r&&n<10;){let o=r.stack();if(o){let s=o.matchAll(cz),a=!1;for(let[,i]of s)a=!0,t.push(` at ${r.name} (${i})`);a||t.push(` at ${r.name} (${o.replace(/^at /,\"\")})`)}else t.push(` at ${r.name}`);r=r.parent,n++}return t.join(`\n`)},fm=e=>Zy(e).map(pk).join(`\n`),pk=e=>e.cause?`${e.stack} {\n${dk(e.cause,\" \")}\n}`:e.stack,dk=(e,t)=>{let r=e.stack.split(`\n`),n=`${t}[cause]: ${r[0]}`;for(let o=1,s=r.length;o{try{return globalThis.process?.getBuiltinModule?.(\"node:async_hooks\")?.AsyncResource}catch{return}})(),mk=()=>ZA===void 0?void 0:new ZA(\"effect/Fiber\"),ds=()=>globalThis[pc],Xd=class{constructor(t,r=!0){this.setContext(t),this.id=++pz.id,this.currentOpCount=0,this.interruptible=r,this._stack=[],this._observers=void 0,this._exit=void 0,this._children=void 0,this._interruptedCause=void 0,this._yielded=void 0,this._running=!1,this._deferredInterrupt=!1,this._parent=void 0,this._asyncContext=void 0,this.cache.runtimeMetrics?.recordFiberStart(this.context)}get[Yy](){return lz}get currentDispatcher(){return this._dispatcher??=this.cache.scheduler.makeDispatcher()}getRef(t){return Fe(this.context,t)}addObserver(t){return this._exit?(t(this._exit),$n):(this._observers===void 0?this._observers=[t]:this._observers.push(t),()=>this.removeObserver(t))}removeObserver(t){if(this._exit||this._observers===void 0)return;let r=this._observers.indexOf(t);r>=0&&this._observers.splice(r,1)}interruptUnsafe(t,r){if(this._exit)return;let n=Zs(t);this.cache.stackFrame&&(n=yc(n,Za(xd,this.cache.stackFrame))),r&&(n=yc(n,r)),this._interruptedCause=this._interruptedCause?za(this._interruptedCause,n):n,this.interruptible&&(this._running?this._deferredInterrupt=!0:this.evaluate(De(this._interruptedCause)))}pollUnsafe(){return this._exit}evaluate(t){if(this._exit)return;if(this._asyncContext!==void 0){let o=this._asyncContext;return this._asyncContext=void 0,o.runInAsyncScope(this.evaluate,this,t)}else if(this._yielded!==void 0){let o=this._yielded;this._yielded=void 0,o()}let r=this.runLoop(t);if(r===ja){this._asyncContext=mk();return}let n=jy.interruptChildren&&jy.interruptChildren(this);if(n!==void 0)return this.evaluate(V(n,()=>r));if(this._exit=r,this.cache.runtimeMetrics?.recordFiberEnd(this.context,this._exit),this._parent&&(this._parent._children?.delete(this),this._parent=void 0),this._observers!==void 0){let o=this._observers;this._observers=void 0;for(let s=0;si)}if(s=a.tracerContext?a.tracerContext(s,this):s[Ke](this),s===ja){let i=this._yielded;if(Da in i)return this._deferredInterrupt=!1,this._yielded=void 0,i;if(this._deferredInterrupt){this._yielded=void 0,i();continue}return ja}}catch(a){if(!k(s,Ke))return Er(`Fiber.runLoop: Not a valid effect: ${String(s)}`);s=Er(a)}}finally{this._running=n,globalThis[pc]=r}}getCont(t){if(this._deferredInterrupt)return this._deferredInterrupt=!1,mz;for(;;){let r=this._stack.pop();if(!r)return;let n=r[La];if(n!==void 0){let o=n.call(r,this);if(o)return o[t]=o,o}if(r[t])return r}}succeedWith(t){if((++this.currentOpCount&hz-1)===0)return Ae(t);let r=this.getCont(_t);return r?r[_t](t,this):this.yieldWith(Ae(t))}yieldWith(t){return this._yielded=t,ja}children(){return this._children??=new Set}pipe(){return Z(this,arguments)}setContext(t){let r=this.context;if(this.context=t,r!==void 0&&nA(r,t))return;let n=t.cacheRoot,o=n._fiberCache??=dz(t);this.cache?.scheduler!==o.scheduler&&(this._dispatcher=void 0),this.cache=o}get currentSpanLocal(){let t=this.cache.span;return t?._tag===\"Span\"?t:void 0}},dz=e=>{let t=Df(e,Py);return{scheduler:Fe(e,zf),tracer:t,tracerContext:t?t.context:void 0,tracerEnabled:Fe(e,Zd),span:Df(e,Ry),logLevel:Fe(e,Ny),minimumLogLevel:Fe(e,Uy),stackFrame:Fe(e,qf),runtimeMetrics:Df(e,GA),maxOpsBeforeYield:Fe(e,DA),preventYield:Fe(e,LA)}},mz={[_t](e,t){return De(t._interruptedCause)},[Qr](e,t){return De(t._interruptedCause)}},hz=32,jy={interruptChildren:void 0},lm=e=>{if(!e.cache.stackFrame)return;let t=new Map;return t.set(ey.key,e.cache.stackFrame),Oo(t)},xz=e=>{if(!(e._children===void 0||e._children.size===0))return Oc(e._children)},Qy=e=>{let t=e;return t._exit?D(t._exit):ht(r=>t._exit?r(D(t._exit)):z(e.addObserver(n=>r(D(n)))))},pm=e=>ht(t=>{let r=e[Symbol.iterator](),n=[],o;function s(){let a=r.next();for(;!a.done;){if(a.value._exit){n.push(a.value._exit),a=r.next();continue}o=a.value.addObserver(i=>{n.push(i),s()});return}t(D(n))}return s(),z(()=>o?.())}),Xy=e=>{let t=e;return t._exit?t._exit:ht(r=>t._exit?r(t._exit):z(e.addObserver(r)))},hk=e=>ht(t=>{let r=Array.from(e);if(r.length===0)return t(D(Pt()));let n=new Array(r.length),o=Pt(),s=()=>{for(let c=0;c{if(a++,f._tag===\"Failure\")return i=!0,s(),t(f);n[c]=f.value,a===r.length&&t(D(n))};o.push(u),r[c].addObserver(u)}return z(()=>{i=!0,s()})}),rl=e=>re(t=>xk(e,t.id)),xk=l(e=>k(e[0],Yy),(e,t,r)=>re(n=>{let o=lm(n);return o=o&&r?Ud(o,r):o??r,e.interruptUnsafe(t,o),nl(Qy(e))})),Oc=e=>re(t=>{let r=lm(t),n=Pt();for(let o of e)o.interruptUnsafe(t.id,r),n.push(o);return nl(pm(n))});var D=Ae,De=Ht,Qe=en,z=En({op:\"Sync\",[Ke](e){let t=this[ue](),r=e.getCont(_t);return r?r[_t](t,e):e.yieldWith(Ae(t))}}),ne=En({op:\"Suspend\",[Ke](e){return this[ue]()}}),eS=l(e=>e.length>=2||wc(e[0]),(e,t)=>fe(e)?Qe(t?t():new Bn(\"Effect.fromOption: Option.none\")):D(e.value)),Pc=wr({onFailure:Qe,onSuccess:D}),gk=e=>e==null?Qe(new Bn):D(e),tS=En({op:\"Yield\",[Ke](e){let t=!1;return e.currentDispatcher.scheduleTask(()=>{t||e.evaluate(pr)},this[ue]??0),e.yieldWith(()=>{t=!0})}}),Mc=tS(0),rS=e=>D(O(e)),oi=D(v()),yk=e=>fe(e)?oi:ke(e.value,O),Sk=e=>ne(()=>De(e())),Ys=e=>Er(e),dm=e=>ne(()=>Qe(e())),pe=D(void 0);var bk=e=>{let t=typeof e==\"function\"?e:e.try,r=typeof e==\"function\"?n=>new Rc(n,\"An error occurred in Effect.try\"):e.catch;return ne(()=>{try{return D(t())}catch(n){return Qe(r(n))}})};var nS=e=>oS(function(t,r){e(r).then(n=>t(D(n)),n=>t(Ys(n)))},e.length!==0),Ek=e=>{let t=typeof e==\"function\"?e:e.try,r=typeof e==\"function\"?n=>new Rc(n,\"An error occurred in Effect.tryPromise\"):e.catch;return oS(function(n,o){let s=a=>ne(()=>{try{return Qe(js(()=>r(a)))}catch(i){return Ys(i)}});try{t(o).then(a=>n(D(a)),a=>n(s(a)))}catch(a){n(s(a))}},t.length!==0)},Ik=e=>re(t=>e(t.id)),Tk=re(D),wk=Ik(D),oS=(function(){let e=Xr({op:\"Async\",[Ke](r){let n=!1,o=!1,s=this.withSignal?new AbortController:void 0,a=this.register.call(r.cache.scheduler,i=>{n||(n=!0,o?r.evaluate(i):o=i)},s?.signal);return o!==!1?o:(o=!0,r._yielded=()=>{n=!0},s===void 0&&a===void 0?ja:(r._stack.push(gz(()=>(n=!0,s?.abort(),a??pr))),ja))}}),t=function(r,n){this.register=r,this.withSignal=n};return t.prototype=e,function(r,n){return new t(r,n)}})(),gz=En({op:\"AsyncFinalizer\",[La](e){e.interruptible&&(e.interruptible=!1,e._stack.push(fl))},[Qr](e,t){return am(e)?V(Ov(Ht(e),this[ue]()),()=>De(e)):De(e)}}),ht=e=>oS(e,e.length>=2),mm=ht($n),Ck=(...e)=>{if(e.length===1){let n=e[0];return ne(()=>ni(n()))}let[t,r]=e;return ne(()=>ni(r.call(t.self)))},sS=(e,...t)=>{let r=t.length===0?function(){return ne(()=>ni(e.apply(this,arguments)))}:function(){let n=ne(()=>ni(e.apply(this,arguments)));for(let o=0;oObject.defineProperty(t,\"length\",{value:e,configurable:!0}),YA=Dy(2),Ak=function(){let e=typeof arguments[0]==\"string\",t=e?arguments[0]:\"Effect.fn\",r=e?arguments[1]:void 0,n=Co(),o;return n!==0&&(Yr(2),o=new globalThis.Error,Yr(n)),e?(s,...a)=>QA(t,s,o,a,e,r):QA(t,arguments[0],o,Array.prototype.slice.call(arguments,1),e,r)},QA=(e,t,r,n,o,s)=>{let a=typeof t==\"function\"?t:n.shift().bind(t.self),i=`${e} (definition)`,c=r?YA(()=>r.stack):bn;return aS(a.length,function(...u){let f=ne(()=>{let m=a.apply(this,arguments);return ie(m)?m:ni(m)});for(let m=0;mJy(f,m)):f,qf,m=>({name:e,stack:d?YA(()=>d.stack):bn,parent:{name:i,stack:c,parent:m}}))})},kk=(e,...t)=>aS(e.length,t.length===0?function(){return XA(()=>e.apply(this,arguments))}:function(){let r=XA(()=>e.apply(this,arguments));for(let n of t)r=n(r,...arguments);return r}),XA=e=>{try{let t=e(),r;for(;;){let n=t.next(r);if(n.done)return D(n.value);let o=n.value;if(o&&o._tag===\"Success\"){r=o.value;continue}else{if(o&&o._tag===\"Failure\")return n.value;{let s=!0;return ne(()=>s?(s=!1,V(n.value,a=>ni(t,a))):ne(()=>ni(e())))}}}}catch(t){return Ys(t)}},ni=(function(){let e=Xr({op:\"Iterator\",[_t](r,n){let o=this.iterator;for(;;){let s=o.next(r);if(s.done)return D(s.value);if(Ne(s.value)){if(s.value._tag===\"Failure\")return s.value}else return n._stack.push(this),s.value;r=s.value.value}},[Ke](r){return this[_t](this.initial,r)}}),t=function(r,n){this.iterator=r,this.initial=n};return t.prototype=e,function(r,n){return new t(r,n)}})(),Ar=l(2,(e,t)=>new ms(e,iS,D(t))),hm=function(e){return e._stack.push(this),this[ue]},yz=Xr({op:\"OnSuccess\",[Ke]:hm}),ms=function(e,t,r){this[ue]=e,this[_t]=t,this.payload=r};ms.prototype=yz;var iS=function(){return this.payload},vk=function(e,t){return t.succeedWith(this.payload)},xm=(()=>{let e=\"~effect/Effect/stackProbe\";return{[e]:function(){return new Error().stack}}[e]()?.includes(\"[as \"+e+\"]\")===!0})(),Sz=function(e,t){let r=this.payload;return t.succeedWith(xm?r(e):js(()=>r(e)))},Ok=function(e){let t=this.payload;return t(e)},bz=function(e){let t=this.payload;return new ms(t(e),vk,e)},Ez=function(e){return new ms(this.payload,vk,e)},Fc=e=>ke(e,O),_k=e=>Dc(e,{onFailure:D,onSuccess:Qe}),Fo=l(2,(e,t)=>new ms(e,ie(t)?iS:Ok,t)),si=l(2,(e,t)=>new ms(e,ie(t)?Ez:bz,t)),nl=e=>new ms(e,iS,pr),Rk=e=>nn(e,Qe),cS=(e,t)=>re(r=>{let n=new Set;return $c(r,()=>n.size===0?void 0:Oc(n)),ht(o=>{let s=Ye(e),a=s.length,i=0,c=!1,u=[],f=(p,d,m)=>{if(i++,p._tag===\"Failure\"){u.push(...p.cause.reasons),i>=a&&o(De(cs(u)));return}let x=!c;c=!0,o(p),x&&t?.onWinner&&t.onWinner({fiber:d,index:m,parentFiber:r})};for(let p=0;p{n.delete(d),f(m,d,p)}),c)break}})}),uS=(e,t)=>re(r=>{let n=new Set;return $c(r,()=>n.size===0?void 0:Oc(n)),ht(o=>{let s=!1,a=0;for(let i of e){if(s)break;let c=a++,u=ra(r,i,!0,!0,!1);n.add(u),u.addObserver(f=>{n.delete(u);let p=!s;s=!0,o(f),p&&t?.onWinner&&t.onWinner({fiber:u,index:c,parentFiber:r})})}})}),Pk=l(e=>ie(e[1]),(e,t,r)=>cS([e,t],r)),fS=l(e=>ie(e[1]),(e,t,r)=>uS([e,t],r)),V=l(2,(e,t)=>new ms(e,Ok,t)),Mk=l(2,(e,t)=>Ne(e)?e._tag===\"Success\"?t.onSuccess(e.value):t.onFailure(e.cause):Mo(e,t)),Ne=e=>e[Da]!==void 0,Fk=l(2,(e,t)=>Ne(e)?e._tag===\"Success\"?t(e.value):e:V(e,t)),Nk=e=>V(e,R),ke=l(2,(e,t)=>new ms(e,Sz,t)),Uk=l(2,(e,t)=>Ne(e)?mS(e,t):ke(e,t)),Dk=l(2,(e,t)=>Ne(e)?hS(e,t):wS(e,t)),Lk=l(2,(e,t)=>Ne(e)?xS(e,t):CS(e,t)),$k=l(2,(e,t)=>{if(Ne(e)){if(e._tag===\"Success\")return e;let r=Ur(e.cause);return ce(r)?e:t(r.success)}return gs(e,t)}),Nc=e=>Ht(Zs(e)),Qs=e=>e._tag===\"Success\",Bk=e=>e._tag===\"Success\"?B(e):W(e),jk=e=>e._tag===\"Success\"?B(e.value):W(e),lS=e=>e._tag===\"Failure\",zk=e=>e._tag===\"Failure\"?B(e):W(e),ol=e=>e._tag===\"Failure\"?B(e.cause):W(e),pS=wy(ol,Ur),qk=wy(ol,tl),dS=e=>e._tag===\"Failure\"&&am(e.cause),Hk=e=>e._tag===\"Failure\"&&sm(e.cause),Wk=e=>e._tag===\"Failure\"&&Gy(e.cause),pr=Ae(void 0),mS=l(2,(e,t)=>e._tag===\"Success\"?Ae(t(e.value)):e),hS=l(2,(e,t)=>{if(e._tag===\"Success\")return e;let r=Ur(e.cause);return ce(r)?e:en(t(r.success))}),xS=l(2,(e,t)=>{if(e._tag===\"Success\")return Ae(t.onSuccess(e.value));let r=Ur(e.cause);return ce(r)?e:en(t.onFailure(r.success))}),Iz=l(2,(e,t)=>Qs(e)?Ae(t):e),Jk=l(2,(e,t)=>Qs(e)?t:e),Kk=l(2,(e,t)=>Qs(e)?t.onSuccess(e.value):t.onFailure(e.cause)),Gk=Iz(void 0),gS=e=>{let t=[];for(let r of e)r._tag===\"Failure\"&&t.push(...r.cause.reasons);return t.length===0?pr:Ht(cs(t))},Vk=e=>Qs(e)?O(e.value):v(),Zk=e=>lS(e)?O(e.cause):v(),Yk=e=>{let t=pS(e);return ce(t)?v():O(t.success)},Qk=e=>e,Xk=e=>re(t=>D(Nd(t.context,e))),Tz=e=>re(t=>eS(Nd(t.context,e))),Uc=l(2,(e,t)=>re(r=>{let n=r.context,o=t(n);return n===o||(r.setContext(o),$c(r,()=>{r.setContext(n)})),e})),hs=l(3,(e,t,r)=>Uc(e,n=>{let o=_o(n,t),s=r(o);return o===s?n:Jt(n,t,s)})),ev=(e,t,r)=>ii(re(n=>{let o=_o(n.context,e),s=t(o);return n.setContext(Jt(n.context,e,s)),ta(_o(n.context,ea),a=>{let i=Nd(n.context,e);if(fe(i))return pe;let c=i.value,u;if(r?.reset===void 0){if(c!==s)return pe;u=o}else u=r.reset(o,s,c);return n.setContext(Jt(n.context,e,u)),pe})})),yS=()=>wz,wz=re(e=>D(e.context)),Xs=e=>re(t=>e(t.context)),tv=l(2,(e,t)=>Uc(e,je(t))),rn=l(2,(e,t)=>Ne(e)?e:Uc(e,Ud(t))),kr=function(){return arguments.length===1?l(2,(e,t)=>ek(e,arguments[0],t)):l(3,(e,t,r)=>ek(e,t,r)).apply(this,arguments)},ek=(e,t,r)=>Uc(e,Jt(t,r)),gm=l(3,(e,t,r)=>V(r,n=>kr(e,t,n))),rv=l(e=>ie(e[1]),(e,t,r)=>SS(e,t,(n,o)=>[n,o],r)),SS=l(e=>ie(e[1]),(e,t,r,n)=>n?.concurrent?ke(ll([e,t],{concurrency:2}),([o,s])=>r(o,s)):V(e,o=>ke(t,s=>r(o,s)))),nv=l(e=>ie(e[0]),(e,t,r)=>BS(e,t,r?n=>Qe(r(n)):()=>Qe(new Bn))),ov=l(2,(e,t)=>V(t,r=>r?Fc(e):oi)),bS=l(2,(e,t)=>Array.from({length:t},()=>e)),sv=l(e=>ie(e[0]),(e,t,r)=>ll(bS(e,t),r)),ym=l(e=>ie(e[0]),(e,t)=>No({while:Sr,body:je(t?.disableYield?e:V(e,r=>Mc)),step:$n})),nn=l(2,(e,t)=>new av(e,t.length!==1?r=>t(r):t)),Cz=Xr({op:\"OnFailure\",[Ke]:hm}),av=function(e,t){this[ue]=e,this[Qr]=t};av.prototype=Cz;var ES=l(3,(e,t,r)=>nn(e,n=>t(n)?r(n):De(n))),xs=l(3,(e,t,r)=>nn(e,n=>{let o=t(n);return ce(o)?De(o.failure):r(o.success,n)})),gs=l(2,(e,t)=>xs(e,Ur,r=>t(r))),iv=e=>Dc(e,{onFailure:t=>gd(t)?oi:Qe(t),onSuccess:rS}),cv=l(2,(e,t)=>xs(e,tl,t)),uv=l(2,(e,t)=>nn(e,r=>Fo(t(r),De(r)))),fv=l(3,(e,t,r)=>ES(e,t,n=>Fo(r(n),De(n)))),Sm=l(3,(e,t,r)=>nn(e,n=>{let o=t(n);return ce(o)?De(n):Fo(r(o.success,n),De(n))})),IS=l(2,(e,t)=>Sm(e,Ur,r=>t(r))),lv=l(3,(e,t,r)=>{let n=Array.isArray(t)?o=>k(o,\"_tag\")&&t.includes(o._tag):At(t);return IS(e,o=>n(o)?r(o):pe)}),pv=l(2,(e,t)=>Sm(e,tl,r=>t(r))),sl=l(e=>ie(e[0]),(e,t,r,n)=>nn(e,o=>{let s=Ur(o);return ce(s)?De(s.failure):t(s.success)?r(s.success):n?n(s.success):De(o)})),bm=l(e=>ie(e[0]),(e,t,r,n)=>nn(e,o=>{let s=Ur(o);if(ce(s))return De(s.failure);let a=t(s.success);return ce(a)?n?n(a.failure):De(o):r(a.success)})),TS=l(e=>ie(e[0]),(e,t,r,n)=>{let o=Array.isArray(t)?s=>k(s,\"_tag\")&&t.includes(s._tag):At(t);return sl(e,o,r,n)}),dv=l(e=>ie(e[0]),(e,t,r)=>{let n;return bm(e,o=>(n??=Object.keys(t),k(o,\"_tag\")&&br(o._tag)&&n.includes(o._tag)?B(o):W(o)),o=>t[o._tag](o),r)}),mv=l(e=>ie(e[0]),(e,t,r,n,o)=>sl(e,s=>At(s,t)&&k(s,\"reason\")&&(o!==void 0||At(s.reason,r)),s=>{let a=s.reason;return At(a,r)?n(a,s):o?o(a,s):Qe(s)})),hv=l(e=>ie(e[0]),(e,t,r,n)=>{let o;return sl(e,s=>At(s,t)&&k(s,\"reason\")&&k(s.reason,\"_tag\")&&br(s.reason._tag)&&(n!==void 0||(o??=Object.keys(r)).includes(s.reason._tag)),s=>{let a=s.reason;return o??=Object.keys(r),o.includes(a._tag)?r[a._tag](a,s):n?n(a,s):Qe(s)})}),xv=l(2,(e,t)=>bm(e,r=>At(r,t)&&k(r,\"reason\")?B(r.reason):W(r),Qe)),wS=l(2,(e,t)=>gs(e,r=>dm(()=>t(r)))),CS=l(2,(e,t)=>Dc(e,{onFailure:r=>dm(()=>t.onFailure(r)),onSuccess:r=>z(()=>t.onSuccess(r))})),AS=e=>gs(e,Ys),gv=l(2,(e,t)=>gs(e,r=>z(()=>t(r)))),yv=e=>ne(()=>{let t=e[Symbol.iterator](),r=t.next();if(r.done)return Ys(new Error(\"Received an empty collection of effects\"));function n(o){let s=t.next();return s.done?o.value:gs(o.value,a=>n(s))}return n(r)}),kS=e=>gs(e,t=>V(Mc,()=>kS(e))),Sv=l(e=>ie(e[0]),(e,t)=>{if(!t?.log)return Dc(e,{onFailure:n=>pe,onSuccess:n=>pe});let r=Jn(t.log===!0?void 0:t.log);return Mo(e,{onFailure(n){let o=Vy(n);return ce(o)?De(o.failure):t.message===void 0?r(n):r(t.message,n)},onSuccess:n=>pe})}),Em=l(e=>ie(e[0]),(e,t)=>{if(!t?.log)return Mo(e,{onFailure:n=>pe,onSuccess:n=>pe});let r=Jn(t.log===!0?void 0:t.log);return Mo(e,{onFailure:n=>t.message===void 0?r(n):r(t.message,n),onSuccess:n=>pe})}),bv=e=>Im(e,{onFailure:v,onSuccess:O}),Vs=e=>al(e,{onFailure:W,onSuccess:B}),Mo=l(2,(e,t)=>new Ev(e,t.onSuccess.length!==1?r=>t.onSuccess(r):t.onSuccess,t.onFailure.length!==1?r=>t.onFailure(r):t.onFailure)),Az=Xr({op:\"OnSuccessAndFailure\",[Ke]:hm}),Ev=function(e,t,r){this[ue]=e,this[_t]=t,this[Qr]=r};Ev.prototype=Az;var kz=function(e,t){let r=this.payload;return t.succeedWith(xm?r.onSuccess(e):js(()=>r.onSuccess(e)))},Iv=(e,t)=>{let r=Xr({op:e,[Ke]:hm,[_t]:kz,[Qr]:t}),n=function(o,s){this[ue]=o,this.payload=s};return n.prototype=r,n},vz=Iv(\"Match\",function(e,t){let r=e.reasons.find(jn);if(r===void 0)return De(e);let n=this.payload;return t.succeedWith(xm?n.onFailure(r.error):js(()=>n.onFailure(r.error)))}),Oz=Iv(\"MatchCause\",function(e,t){let r=this.payload;return t.succeedWith(xm?r.onFailure(e):js(()=>r.onFailure(e)))}),vS=l(2,(e,t)=>new Oz(e,t)),Dc=l(2,(e,t)=>Mo(e,{onFailure:r=>{let n=r.reasons.find(jn);return n?t.onFailure(n.error):De(r)},onSuccess:t.onSuccess})),Im=l(2,(e,t)=>new vz(e,t)),al=l(2,(e,t)=>{if(Ne(e)){if(e._tag===\"Success\")return Ae(t.onSuccess(e.value));let r=Ur(e.cause);return ce(r)?e:Ae(t.onFailure(r.success))}return Im(e,t)}),Tv=l(2,(e,t)=>Ne(e)?e._tag===\"Success\"?Ae(t.onSuccess(e.value)):Ae(t.onFailure(e.cause)):vS(e,t)),il=e=>Ne(e)?Ae(e):_z(e),_z=En({op:\"Exit\",[Ke](e){return e._stack.push(this),this[ue]},[_t](e,t,r){return t.succeedWith(r??Ae(e))},[Qr](e,t,r){return t.succeedWith(r??Ht(e))}}),wv=al({onFailure:()=>!0,onSuccess:()=>!1}),Cv=al({onFailure:()=>!1,onSuccess:()=>!0}),Av=l(2,(e,t)=>Fo(Bc(t),e)),OS=l(2,(e,t)=>V(_S(e,t.duration),r=>fe(r)?t.orElse():D(r.value))),Rz=e=>new Vf(`Operation timed out after '${Ty(e)}'`),kv=l(2,(e,t)=>{let r=Ge(t);return OS(e,{duration:r,orElse:()=>Qe(Rz(r))})}),_S=l(2,(e,t)=>fS(Fc(e),Ar(Bc(t),v()))),vv=e=>Wn(t=>{let r=t.monotonicTimeNanosUnsafe();return ke(e,n=>[Ro(t.monotonicTimeNanosUnsafe()-r),n])}),zy=\"~effect/Scope\",qy=\"~effect/Scope/Closeable\",ea=lr(\"effect/Scope\"),RS=(e,t)=>re(r=>(Nz(r),Tm(e,t)??pe)),Tm=(e,t)=>{let r=e.state;if(r._tag===\"Closed\"||(e.state={_tag:\"Closed\",exit:t},e.parent!==void 0&&tm(e.parent,e),r._tag===\"Empty\"))return;if(r.finalizer!==void 0)return em(r.finalizer,t);let n=r.finalizers;return n.size===1?em(n.values().next().value,t):Pz(e,n,t)},em=(e,t)=>{try{return e(t)}catch(r){return Er(r)}},Ov=(e,t)=>Qs(e)?t:nn(t,r=>De(za(e.cause,r))),Pz=sS(function*(e,t,r){let n=[],o=[],s=Array.from(t.values()),a=ds();for(let i=s.length-1;i>=0;i--){let c=s[i];e.strategy===\"sequential\"?n.push(yield*il(em(c,r))):o.push(ra(a,em(c,r),!0,!0,\"inherit\"))}return o.length>0&&(n=yield*pm(o)),yield*gS(n)});var _v=(e,t)=>{let r=Pv(t,e);return e.state._tag===\"Closed\"?(r.state=e.state,r):(Lc(e,r,n=>RS(r,n)),r)},ta=(e,t)=>ne(()=>e.state._tag===\"Closed\"?t(e.state.exit):(Lc(e,{},t),pe)),Rv=(e,t)=>ta(e,je(t)),Lc=(e,t,r)=>{if(e.state._tag===\"Empty\")e.state={_tag:\"Open\",finalizerKey:t,finalizer:r,finalizers:void 0};else if(e.state._tag===\"Open\"){let n=e.state;n.finalizer!==void 0&&(n.finalizers=new Map([[n.finalizerKey,n.finalizer]]),n.finalizerKey=void 0,n.finalizer=void 0),n.finalizers.set(t,r)}},tm=(e,t)=>{if(e.state._tag!==\"Open\")return;let r=e.state;r.finalizerKey===t?e.state=Hy:r.finalizers!==void 0&&(r.finalizers.delete(t),r.finalizers.size===0&&(e.state=Hy))},wm=e=>Pv(e,void 0),Pv=(e=\"sequential\",t)=>({[qy]:qy,[zy]:zy,strategy:e,parent:t,state:Hy}),Hy={_tag:\"Empty\"};var cl=ea,Mv=kr(ea),PS=e=>re(t=>{let r=t.context,n=wm();return t.setContext(Jt(t.context,ea,n)),$c(t,o=>(t.setContext(r),Tm(n,o))),e});var Cm=e=>ne(()=>{let t=wm();return on(e(t),r=>ne(()=>Tm(t,r)??pe))}),ai=(e,t,r)=>Xs(n=>Hn(o=>V(cl,s=>si(r?.interruptible?o(e):e,a=>ta(s,i=>rn(t(a,i),n)))))),MS=e=>V(cl,t=>Xs(r=>ta(t,n=>rn(e(n),r)))),Fv=(function(){let e=Xr({op:\"OnExit\",[Ke](r){return r._stack.push(this),this.effect},[La](r){r.interruptible&&this.interruptible!==!0&&(r._stack.push(fl),r.interruptible=!1)},[_t](r,n,o){o??=Ae(r);let s;try{s=this.onExit(o)}catch(a){s=Er(a)}return s?V(s,a=>o):o},[Qr](r,n,o){o??=Ht(r);let s;try{s=this.onExit(o)}catch(a){s=Er(a)}return s?V(Ov(o,s),a=>o):o}}),t=function(r,n,o){this.effect=r,this.onExit=n,this.interruptible=o};return t.prototype=e,t})(),qn=(e,t,r)=>new Fv(e,t,r),$c=(e,t)=>{e._stack.push(new Fv(void 0,t,void 0))},on=l(2,qn),Nv=l(2,(e,t)=>on(e,r=>t)),FS=l(3,(e,t,r)=>on(e,n=>t(n)?r(n):pe)),NS=l(3,(e,t,r)=>on(e,n=>{let o=t(n);return ce(o)?pe:r(o.success,n)})),US=l(2,(e,t)=>NS(e,ol,t)),Uv=l(3,(e,t,r)=>FS(e,n=>n._tag!==\"Failure\"?!1:t(n.cause),n=>r(n.cause))),DS=l(3,(e,t,r)=>on(e,n=>{if(n._tag!==\"Failure\")return pe;let o=t(n.cause);return ce(o)?pe:r(o.success,n.cause)})),Dv=l(2,(e,t)=>DS(im,t)(e)),ul=(e,t,r)=>Hn(n=>V(e,o=>qn(ne(()=>n(t(o))),s=>r(o,s),!0))),Lv=e=>ai(e,t=>k(t,Symbol.asyncDispose)?nS(()=>t[Symbol.asyncDispose]()):z(()=>t[Symbol.dispose]())),LS=l(2,(e,t)=>z(()=>$v(e,typeof t==\"function\"?r=>Cr(Ge(t(r))):je(Cr(Ge(t)))))),Mz=je(1/0),$v=(e,t)=>{let r=0,n,o,s=(a,i)=>(i.awaiters++,$c(a,()=>{if(!(--i.awaiters>0||o!==i))return o=void 0,rl(i.fiber)}),Xy(i.fiber));return[re(a=>{if(o!==void 0)return s(a,o);if(n!==void 0&&(r===1/0||a.getRef(Nr).currentTimeMillisUnsafe(){if(o!==c||(o=void 0,dS(u)))return;let f=t(u);r=f===1/0?1/0:i.currentTimeMillisUnsafe()+f,n=u}),!0,!0),c.fiber._exit??s(a,c)}),z(()=>{n=void 0})]},Bv=l(2,(e,t)=>ke(LS(e,t),r=>r[0])),jv=e=>z(()=>$v(e,Mz)[0]),zv=re(e=>De(Zs(e.id))),ii=e=>re(t=>(t.interruptible&&(t.interruptible=!1,t._stack.push(fl)),e)),qv=En({op:\"SetInterruptible\",[La](e){if(e.interruptible=this[ue],e._interruptedCause&&e.interruptible)return()=>De(e._interruptedCause)}}),fl=qv(!0),Fz=qv(!1),Hv=e=>{if(e.interruptible=!0,e._stack.push(Fz),e._interruptedCause)return De(e._interruptedCause)},Nz=e=>{let t=e;t.interruptible&&(t.interruptible=!1,t._stack.push(fl))};var Am=e=>re(t=>t.interruptible?e:Hv(t)??e),Hn=e=>re(t=>t.interruptible?(t.interruptible=!1,t._stack.push(fl),e(Am)):e(R)),Wv=e=>re(t=>{if(t.interruptible)return e(R);let r=Hv(t),n=e(ii);return r??n}),Jv=ke(ai(z(()=>new AbortController),e=>z(()=>e.abort())),e=>e.signal),ll=(e,t)=>as(e)?t?.mode===\"result\"?tn(e,Vs,t):tn(e,R,t):t?.discard?t.mode===\"result\"?tn(Object.values(e),Vs,t):tn(Object.values(e),R,t):ne(()=>{let r={};return Ar(tn(Object.entries(e),([n,o])=>ke(t?.mode===\"result\"?Vs(o):o,s=>{M(r,n,s)}),{discard:!0,concurrency:t?.concurrency}),r)}),$S=l(e=>as(e[0])&&!ie(e[0]),(e,t,r)=>ke(tn(e,(n,o)=>Vs(t(n,o)),r),n=>Sy(n,R))),Kv=l(3,(e,t,r)=>{let n=Ye(e);return n.length===0?z(t):ne(()=>{let o=0,s=t();return ke(No({while:()=>or(s,n[o],o),step(a){s=a,o++}}),()=>s)})}),Gv=l(e=>as(e[0])&&!ie(e[0]),(e,t,r)=>V($S(e,t,{concurrency:r?.concurrency}),([n,o])=>or(o)?Qe(o):r?.discard?pe:D(n))),Vv=l(e=>as(e[0])&&!ie(e[0]),(e,t)=>ne(()=>{let r=e[Symbol.iterator](),n=r.next();return n.done?D(v()):Zv(r,0,t,n.value)})),Zv=(e,t,r,n)=>V(r(n,t),o=>{if(o)return D(O(n));let s=e.next();return s.done?D(v()):Zv(e,t+1,r,s.value)}),Yv=l(e=>as(e[0])&&!ie(e[0]),(e,t)=>ne(()=>{let r=e[Symbol.iterator](),n=r.next();return n.done?D(v()):Qv(r,0,t,n.value)})),Qv=(e,t,r,n)=>V(r(n,t),o=>{if(mt(o))return D(O(o.success));let s=e.next();return s.done?D(v()):Qv(e,t+1,r,s.value)}),No=En({op:\"While\",[_t](e,t){return this[ue].step(e),this[ue].while()?(t._stack.push(this),this[ue].body()):pr},[Ke](e){return this[ue].while()?(e._stack.push(this),this[ue].body()):pr}}),tn=l(e=>typeof e[1]==\"function\",(e,t,r)=>ne(()=>{let n=pl(r?.concurrency);if(n===1)return Uz(e,t,r);let o=Ye(e),s=o.length;if(s===0)return r?.discard?pe:D([]);let a=r?.discard?void 0:new Array(s),i=Dz({f:t,out:a},o,{concurrency:n});return i?Ar(i,a):D(a)})),Xv=e=>V(e,t=>{let r=t[Symbol.iterator]().next();return r.done?Qe(new Bn):D(r.value)}),Uz=(e,t,r)=>ne(()=>{let n=r?.discard?void 0:[],o=e[Symbol.iterator](),s=o.next(),a=0;return Ar(No({while:()=>!s.done,body:()=>t(s.value,a++),step:i=>{n&&n.push(i),s=o.next()}}),n)}),pl=e=>e===\"unbounded\"?Number.POSITIVE_INFINITY:Math.max(1,e??1),km=()=>e=>{let t=e.onItem,r=e.step,n=(s,a,i,c,u)=>V(il(u),f=>r(s,a[i],f,i)??o(s,a,i+1,c)??pe),o=(s,a,i=0,c=a.length)=>{for(;i{let t=e.onItem,r=e.step;return(n,o,s)=>{let a=0,i=s.end??o.length,c=s.concurrency,u=!1,f,p,d,m,x,y=w=>{let E=Er(w);return m=E,u=!0,p&&p.size>0?V(ii(Oc(Array.from(p))),()=>m??E):E},S=()=>{let w=!1;for(;!m&&a{p.delete(_);try{if(m){if(b._tag===\"Failure\"){let T=b.cause.reasons.filter(C=>C._tag!==\"Interrupt\");if(T.length>0){let C=cs(T);m=Ht(m._tag===\"Failure\"?za(m.cause,C):C)}}}else{let T=r(n,E,b,h);T&&(m=T,S())}if(w){let T=S();T&&d(T)}else u&&p.size===0&&d(m??pe)}catch(T){d(y(T))}}),p.size{f=ds(),p=new Set,x=U,d=_;let h;try{h=S()}catch(b){return _(y(b))}return h?_(h):ne(()=>(m??=pr,V(p?Oc(p):pe,()=>m?._tag===\"Failure\"?m:pe)))})}if(u=!0,m){if(p&&p.size>0){let E=lm(f);p.forEach(U=>U.interruptUnsafe(f.id,E));return}if(d||m._tag===\"Failure\")return m}else if(d)if(p)p.size===0&&d(pe);else return pr};return S()}},vm=()=>e=>eO(e),Dz=eO({onItem(e,t,r){return e.f(t,r)},step(e,t,r,n){if(r._tag===\"Failure\")return r;e.out&&(e.out[n]=r.value)}}),BS=l(3,(e,t,r)=>V(e,n=>t(n)?D(n):r(n))),jS=l(3,(e,t,r)=>V(e,n=>{let o=t(n);return ce(o)?r(o.failure):D(o.success)})),tO=l(e=>ie(e[0]),(e,t,r)=>jS(e,t,r?n=>Qe(r(n)):()=>Qe(new Bn))),rO=l(e=>as(e[0])&&!ie(e[0]),(e,t,r)=>ne(()=>{let n=[];return Ar(tn(e,(o,s)=>{let a=t(o,s);return typeof a==\"boolean\"?(a&&n.push(o),pe):ke(a,i=>{i&&n.push(o)})},{discard:!0,concurrency:r?.concurrency}),n)})),nO=l(e=>as(e[0])&&!ie(e[0]),(e,t)=>ne(()=>{let r=[];for(let n of e){let o=t(n);mt(o)&&r.push(o.success)}return D(r)})),oO=l(e=>as(e[0])&&!ie(e[0]),(e,t,r)=>ne(()=>{let n=[];return Ar(tn(e,o=>ke(t(o),s=>{mt(s)&&n.push(s.success)}),{discard:!0,concurrency:r?.concurrency}),n)})),sO=D({}),aO=Ed(ke),iO=Id(ke,V),cO=bd(ke);var uO=l(e=>ie(e[0]),(e,t)=>re(r=>(Qz(),D(ra(r,e,t?.startImmediately,!1,t?.uninterruptible??!1))))),ra=(e,t,r=!1,n=!1,o=!1)=>{let s=e,a=o===\"inherit\"?s.interruptible:!o,i=new Xd(s.context,a);return n||(s.children().add(i),i._parent=s),r?i.evaluate(t):(i._asyncContext=mk(),s.currentDispatcher.scheduleTask(()=>i.evaluate(t),0)),i},fO=l(e=>ie(e[0]),(e,t)=>re(r=>D(ra(r,e,t?.startImmediately,!0,t?.uninterruptible)))),lO=e=>re(t=>{let r=t._children&&new Set(t._children),n=t.interruptible;return qn(e,o=>{let s=t._children;if(s===void 0||s.size===0)return pe;r&&(s=dA(s,i=>!r.has(i)));let a=nl(pm(s));return n?Am(a):a})}),zS=l(e=>ie(e[0]),(e,t,r)=>re(n=>{let o=ra(n,e,r?.startImmediately,!0,r?.uninterruptible);if(!o._exit)if(t.state._tag!==\"Closed\"){let s={};Lc(t,s,()=>Ik(i=>i===o.id?pe:rl(o))),o.addObserver(()=>tm(t,s))}else o.interruptUnsafe(n.id,lm(n));return D(o)})),pO=l(e=>ie(e[0]),(e,t)=>V(cl,r=>zS(e,r,t))),ys=e=>(t,r)=>{let n=new Xd(r?.scheduler?Jt(e,zf,r.scheduler):e,r?.uninterruptible!==!0);if(n.evaluate(t),n._exit)return n;if(r?.signal)if(r.signal.aborted)n.interruptUnsafe();else{let o=()=>n.interruptUnsafe();r.signal.addEventListener(\"abort\",o,{once:!0}),n.addObserver(()=>r.signal.removeEventListener(\"abort\",o))}return r?.onFiberStart&&r.onFiberStart(n),n},dO=l(2,(e,t)=>e._exit?e:t.state._tag===\"Closed\"?(e.interruptUnsafe(e.id),e):(tm(t,e),Lc(t,e,()=>rl(e)),e.addObserver(()=>tm(t,e)),e)),mO=ys(Ir()),qS=e=>{let t=ys(e);return(r,n)=>{let o=t(r,n);return n?.onExit&&o.addObserver(n.onExit),s=>o.interruptUnsafe(s)}},hO=qS(Ir()),Om=e=>{let t=ys(e);return(r,n)=>{let o=t(r,n);return new Promise(s=>{o.addObserver(a=>s(a))})}},xO=Om(Ir()),HS=e=>{let t=Om(e);return(r,n)=>t(r,n).then(o=>{if(o._tag===\"Failure\")throw um(o.cause);return o.value})},gO=HS(Ir()),_m=e=>{let t=ys(e);return r=>{if(Ne(r))return r;let n=new Qa(\"sync\"),o=t(r,{scheduler:n});return o._dispatcher?.flush(),o._exit??Er(new Xf(o))}},yO=_m(Ir()),WS=e=>{let t=_m(e);return r=>{let n=t(r);if(n._tag===\"Failure\")throw um(n.cause);return n.value}},SO=WS(Ir()),tk=D(!0),rk=D(!1),Wy=class{waiters=[];scheduled=void 0;_isOpen;constructor(t){this._isOpen=t}scheduleUnsafe(t){if(this.waiters.length===0)return tk;if(this.scheduled===void 0)this.scheduled=this.waiters,t.currentDispatcher.scheduleTask(this.flushScheduled,0);else for(let r=0;r{if(this.scheduled===void 0)return;let t=this.scheduled;this.scheduled=void 0;for(let r=0;rthis._isOpen?rk:(this._isOpen=!0,this.scheduleUnsafe(t)));release=re(t=>this._isOpen?rk:this.scheduleUnsafe(t));openUnsafe(){return this._isOpen?!1:(this._isOpen=!0,this.flushWaiters(),!0)}await=ht(t=>this._isOpen?t(pe):(this.waiters.push(t),z(()=>{let r=this.waiters.indexOf(t);r!==-1?this.waiters.splice(r,1):this.scheduled!==void 0&&(r=this.scheduled.indexOf(t),r!==-1&&this.scheduled.splice(r,1))})));closeUnsafe(){return this._isOpen?(this._isOpen=!1,!0):!1}close=z(()=>this.closeUnsafe());whenOpen=t=>V(this.await,()=>t);isOpen(){return this._isOpen}},JS=e=>new Wy(e??!1),bO=e=>z(()=>JS(e)),EO=re(e=>D(e.getRef(My))),IO=l(2,(e,t)=>kr(e,My,t)),TO=kr(Zd),wO=kr(ti),rm=BigInt(0),Lz={_tag:\"Span\",spanId:\"noop\",traceId:\"noop\",sampled:!1,status:{_tag:\"Ended\",startTime:rm,endTime:rm,exit:pr},attributes:new Map,links:[],kind:\"internal\",attribute(){},event(){},end(){},addLinks(){}},$z=e=>Object.assign(Object.create(Lz),e),CO=e=>e?Fe(e.annotations,Vd)?e._tag===\"Span\"?CO(qs(e.parent)):v():O(e):v(),KS=(e,t,r)=>{let n=!e.cache.tracerEnabled||r?.annotations&&Fe(r.annotations,Vd),o=r?.parent!==void 0?O(r.parent):r?.root?v():CO(e.cache.span),s;if(n)s=$z({name:t,parent:o,annotations:Jt(r?.annotations??Ir(),Vd,!0)});else{let a=e.cache.tracer??Fy,i=e.getRef(Nr),c=e.getRef(ti),u=e.getRef(Hf),f=e.getRef(Wf),p=r?.level??e.getRef(WA),d=r?.links!==void 0?[...f,...r.links]:f.length===0?[]:f.slice();s=a.span({name:t,parent:o,annotations:r?.annotations??Ir(),links:d,startTime:c?i.currentTimeNanosUnsafe():rm,kind:r?.kind??\"internal\",root:r?.root??!1,sampled:r?.sampled??(be(o)&&o.value.sampled===!1?!1:!qO(e.getRef(JA),p))});for(let m in u)s.attribute(m,u[m]);if(r?.attributes!==void 0)for(let m in r.attributes)s.attribute(m,r.attributes[m])}return s},AO=(e,t)=>re(r=>D(KS(r,e,t))),Jf=(e,t)=>ii(re(r=>{let n=_o(r.context,ea),o=KS(r,e,t??{}),s=r.getRef(Nr),a=r.getRef(ti);return Ar(ta(n,i=>Rm(o,i,s,a)),o)})),kO=function(){let e=typeof arguments[0]!=\"string\",t=e?arguments[1]:arguments[0],r=Gs(e?arguments[2]:arguments[1]);if(e){let n=arguments[0];return V(Jf(t,r),o=>_c(n,o,r))}return n=>V(Jf(t,r),o=>_c(n,o,r))},Bz=(e,t)=>(t=typeof t==\"function\"?t:bn,hs(qf,r=>({name:e,stack:t,parent:r}))),vO=Hf,OO=Wf,_O=l(e=>ie(e[0]),(e,t,r={})=>{let o=(Array.isArray(t)?t:[t]).map(s=>({span:s,attributes:r}));return hs(e,Wf,s=>[...s,...o])}),Rm=(e,t,r,n)=>z(()=>{e.status._tag!==\"Ended\"&&e.end(n?r.currentTimeNanosUnsafe():rm,t)}),Kf=(e,...t)=>{let r=t.length===1?void 0:t[0],n=t[t.length-1];return re(o=>{let s=KS(o,e,r),a=o.getRef(Nr),i=o.getRef(ti);return $c(o,c=>Rm(s,c,a,i)),n(s)})},Jy=kr(ei),_c=function(){let e=ie(arguments[0]),t=e?arguments[1]:arguments[0],r=e?arguments[2]:arguments[1],n=R;return t._tag===\"Span\"&&(r=Gs(r),n=Bz(t.name,r?.captureStackTrace)),e?Jy(n(arguments[0]),t):o=>Jy(n(o),t)},RO=function(){let e=typeof arguments[0]!=\"string\",t=e?arguments[1]:arguments[0],r=Gs(arguments[2]);if(e){let s=arguments[0];return Kf(t,arguments[2],a=>_c(s,a,r))}let n=typeof arguments[1]==\"function\"?arguments[1]:void 0,o=n?void 0:arguments[1];return(s,...a)=>Kf(t,n?n(...a):o,i=>_c(s,i,r))},PO=l(e=>ie(e[0]),(e,...t)=>hs(e,Hf,r=>{let n=t.length===1?{...r,...t[0]}:{...r};return t.length===1||M(n,t[0],t[1]),n})),MO=(...e)=>re(t=>{let r=t.currentSpanLocal;if(r)if(e.length===1)for(let[n,o]of Object.entries(e[0]))r.attribute(n,o);else r.attribute(e[0],e[1]);return pe}),FO=re(e=>{let t=e.currentSpanLocal;return t?D(t):Qe(new Bn)}),NO=Tz(ei),Nr=we(\"effect/Clock\",{defaultValue:()=>new Ky}),Ly=2**31-1,Ky=class{currentTimeMillisUnsafe(){return Date.now()}currentTimeMillis=z(()=>this.currentTimeMillisUnsafe());currentTimeNanosUnsafe(){return jz()}currentTimeNanos=z(()=>this.currentTimeNanosUnsafe());monotonicTimeNanosUnsafe(){return DO()}monotonicTimeNanos=z(()=>this.monotonicTimeNanosUnsafe());sleep(t){return this.sleepMillis(Cr(t))}sleepMillis(t){return t<=0?Mc:Number.isFinite(t)?ht(r=>{let n=t>Ly?this.sleepMillis(t-Ly):pe,o=setTimeout(()=>r(n),Math.min(t,Ly));return z(()=>clearTimeout(o))}):mm}},UO=BigInt(1e6),DO=(function(){let e=globalThis.process?.hrtime;if(typeof e?.bigint==\"function\")return()=>e.bigint();if(typeof performance<\"u\"&&typeof performance.now==\"function\")return()=>BigInt(Math.round(performance.now()*1e6));let t=BigInt(0);return()=>{let r=BigInt(Date.now())*UO;return r>t&&(t=r),t}})(),jz=(function(){let e=BigInt(1e9),t;return()=>{let r=DO(),n=BigInt(Date.now())*UO;if(t===void 0)t=n-r;else{let o=t+r;(n>o?n-o:o-n)>e&&(t=n-r)}return t+r}})(),Wn=e=>re(t=>e(t.getRef(Nr))),Bc=e=>Wn(t=>t.sleep(Ge(e))),GS=Wn(e=>e.currentTimeMillis);var Gf=\"~effect/Cause/TimeoutError\",LO=e=>k(e,Gf),Vf=class extends us(\"TimeoutError\"){[Gf]=Gf;constructor(t){super({message:t})}},Zf=\"~effect/Cause/IllegalArgumentError\",$O=e=>k(e,Zf),nm=class extends us(\"IllegalArgumentError\"){[Zf]=Zf;constructor(t){super({message:t})}},Yf=\"~effect/Cause/ExceededCapacityError\",BO=e=>k(e,Yf),om=class extends us(\"ExceededCapacityError\"){[Yf]=Yf;constructor(t){super({message:t})}},Qf=\"~effect/Cause/AsyncFiberError\",jO=e=>k(e,Qf),Xf=class extends us(\"AsyncFiberError\"){[Qf]=Qf;constructor(t){super({message:\"An asynchronous Effect was executed with Effect.runSync\",fiber:t})}},el=\"~effect/Cause/UnknownError\",zO=e=>k(e,el),Rc=class extends us(\"UnknownError\"){[el]=el;constructor(t,r){super({message:r,cause:t})}},VS=we(\"effect/Console\",{defaultValue:()=>globalThis.console}),zz=e=>{switch(e){case\"All\":return Number.MIN_SAFE_INTEGER;case\"Fatal\":return 5e4;case\"Error\":return 4e4;case\"Warn\":return 3e4;case\"Info\":return 2e4;case\"Debug\":return 1e4;case\"Trace\":return 0;case\"None\":return Number.MAX_SAFE_INTEGER}},qz=ay(fs,zz),qO=Tc(qz),ZS=we(\"effect/Logger/CurrentLoggers\",{defaultValue:()=>new Set([Zz,Yz])}),Hz=we(\"effect/Logger/LogToStderr\",{defaultValue:yf}),HO=function(){let e=typeof arguments[0]==\"string\"?[[arguments[0],arguments[1]]]:Object.entries(arguments[0]);return ii(re(t=>{let r=t.getRef(ps),n={...r};for(let o=0;o{let s=t.getRef(ps),a={...s};for(let i=0;i{let t=Object.create(Jz);return t.log=e,t},Kz=e=>e.replace(/[\\s=\"]/g,\"_\"),Gz=(e,t)=>`${Kz(e[0])}=${t-e[1]}ms`;var Jn=e=>(...t)=>{let r;for(let n=0,o=t.length;n{let o=e??n.cache.logLevel;if(qO(n.cache.minimumLogLevel,o))return pe;let s=n.getRef(Nr),a=n.getRef(ZS);if(a.size>0){let i=new Date(s.currentTimeMillisUnsafe());for(let c of a)c.log({cause:r,fiber:n,date:i,logLevel:o,message:t})}return pe})};var ri={bold:\"1\",red:\"31\",green:\"32\",yellow:\"33\",blue:\"34\",cyan:\"36\",white:\"37\",gray:\"90\",black:\"30\",bgBrightRed:\"101\"},kre={None:[],All:[],Trace:[ri.gray],Debug:[ri.blue],Info:[ri.green],Warn:[ri.yellow],Error:[ri.red],Fatal:[ri.bgBrightRed,ri.black]};var Vz=e=>`${e.getHours().toString().padStart(2,\"0\")}:${e.getMinutes().toString().padStart(2,\"0\")}:${e.getSeconds().toString().padStart(2,\"0\")}.${e.getMilliseconds().toString().padStart(3,\"0\")}`;var Zz=WO(({cause:e,date:t,fiber:r,logLevel:n,message:o})=>{let s=Array.isArray(o)?o.slice():[o];e.reasons.length>0&&s.push(fm(e));let a=t.getTime(),i=r.getRef(Yd),c=\"\";for(let d of i)c+=` ${Gz(d,a)}`;let u=r.getRef(ps);Object.keys(u).length>0&&s.push(u);let f=r.getRef(VS);(r.getRef(Hz)?f.error:f.log)(`[${Vz(t)}] ${n.toUpperCase()} (#${r.id})${c}:`,...s)}),Yz=WO(({cause:e,fiber:t,logLevel:r,message:n})=>{let o=t.getRef(Nr),s=t.getRef(ps),a=t.cache.span;if(a===void 0||a._tag===\"ExternalSpan\")return;let i={};for(let[c,u]of Object.entries(s))M(i,c,u);i[\"effect.fiberId\"]=t.id,i[\"effect.logLevel\"]=r.toUpperCase(),e.reasons.length>0&&(i[\"effect.cause\"]=fm(e)),a.event(IC(Array.isArray(n)&&n.length===1?n[0]:n),o.currentTimeNanosUnsafe(),i)});function Qz(){jy.interruptChildren??=xz}var YS=D(void 0);var JO=l(e=>ie(e[0]),(e,t)=>US(e,r=>re(n=>(Xz(n,r,t?.defectsOnly),pe)))),Xz=(e,t,r)=>{let n=e.getRef(VA);if(n.size===0||r&&!sm(t))return;let o={cause:t,fiber:e,timestamp:e.getRef(Nr).currentTimeNanosUnsafe()};n.forEach(s=>s.report(o))};var eq=Cf,tq=mc,eb=xc,tb=CC,na=jn,rb=qa,rq=Ha,oa=cs,nq=hd,oq=gc,ci=Xg,sq=Zs,jc=e=>new $a(e),zc=e=>new Af(e),nb=nk,ob=cm,Ss=fk,aq=za,Pm=um,sb=Gy,iq=Vy,ab=Ur,ib=ok,cq=sm,uq=sk,dl=tl,fq=am,lq=ak,pq=ik,dq=im,mq=Zy,cb=fm,hq=gd,xq=kf,gq=Bn,qc=vC,yq=Ba,ub=Wa,Y=_C,Sq=Gf,bq=LO,Eq=Vf,Iq=Zf,Tq=$O,ml=nm,wq=BO,Cq=Yf,fb=om,Aq=Qf,kq=jO,vq=Xf,Oq=el,_q=zO,Rq=Rc,Pq=yc,Mm=ck,Mq=uk,QS=class extends lr()(\"effect/Cause/StackTrace\"){},XS=class extends lr()(\"effect/Cause/InterruptorStackTrace\"){};var N={};Eo(N,{Do:()=>XH,Transaction:()=>Vc,TypeId:()=>WH,abortSignal:()=>yJ,acquireDisposable:()=>iJ,acquireRelease:()=>gi,acquireUseRelease:()=>Y_,addFinalizer:()=>cJ,all:()=>Ym,andThen:()=>_r,annotateCurrentSpan:()=>OJ,annotateLogs:()=>aK,annotateLogsScoped:()=>iK,annotateSpans:()=>vJ,as:()=>Tt,asSome:()=>eh,asVoid:()=>ia,awaitAllChildren:()=>qJ,bind:()=>rW,bindTo:()=>eW,cached:()=>dJ,cachedInvalidateWithTTL:()=>hJ,cachedWithTTL:()=>mJ,callback:()=>Yc,catch:()=>ca,catchCause:()=>rt,catchCauseFilter:()=>EW,catchCauseIf:()=>bW,catchDefect:()=>Wb,catchEager:()=>ru,catchFilter:()=>yW,catchIf:()=>gW,catchNoSuchElement:()=>SW,catchReason:()=>mW,catchReasons:()=>hW,catchTag:()=>pW,catchTags:()=>dW,clockWith:()=>Si,context:()=>hi,contextWith:()=>Ml,currentParentSpan:()=>RJ,currentSpan:()=>_J,delay:()=>Jb,die:()=>an,effectify:()=>yK,ensuring:()=>yi,eventually:()=>kW,exit:()=>vn,fail:()=>P,failCause:()=>ze,failCauseSync:()=>Is,failSync:()=>Xm,fiber:()=>HJ,fiberId:()=>WJ,filter:()=>Gb,filterMap:()=>qW,filterMapEffect:()=>Vb,filterMapOrElse:()=>WW,filterMapOrFail:()=>KW,filterOrElse:()=>HW,filterOrFail:()=>JW,findFirst:()=>VH,findFirstFilter:()=>ZH,firstSuccessOf:()=>UW,flatMap:()=>I,flatMapEager:()=>st,flatten:()=>qb,flip:()=>fW,fn:()=>QJ,fnUntraced:()=>Se,fnUntracedEager:()=>oE,forEach:()=>sa,forever:()=>$t,forkChild:()=>eu,forkDetach:()=>zJ,forkIn:()=>Ut,forkScoped:()=>tu,fromNullishOr:()=>iW,fromOption:()=>sW,fromResult:()=>Pl,gen:()=>dr,head:()=>YH,ignore:()=>RW,ignoreCause:()=>PW,interrupt:()=>_n,interruptible:()=>xJ,interruptibleMask:()=>gJ,isEffect:()=>jo,isFailure:()=>XW,isSuccess:()=>eJ,let:()=>tW,linkSpans:()=>FJ,log:()=>XJ,logDebug:()=>nK,logError:()=>nE,logFatal:()=>eK,logInfo:()=>rK,logTrace:()=>oK,logWarning:()=>tK,logWithLevel:()=>ih,makeSpan:()=>eE,makeSpanScoped:()=>NJ,map:()=>j,mapBoth:()=>IW,mapBothEager:()=>Vt,mapEager:()=>Dr,mapError:()=>th,mapErrorEager:()=>Ul,match:()=>Zb,matchCause:()=>VW,matchCauseEager:()=>ZW,matchCauseEffect:()=>zo,matchCauseEffectEager:()=>YW,matchEager:()=>GW,matchEffect:()=>QW,never:()=>mi,onError:()=>Vn,onErrorFilter:()=>fJ,onErrorIf:()=>uJ,onExit:()=>un,onExitFilter:()=>pJ,onExitIf:()=>lJ,onExitPrimitive:()=>Fl,onInterrupt:()=>Xb,option:()=>uW,orDie:()=>TW,orElseSucceed:()=>NW,partition:()=>JH,promise:()=>Qm,provide:()=>tJ,provideContext:()=>On,provideService:()=>qo,provideServiceEffect:()=>aJ,race:()=>zW,raceAll:()=>Kb,raceAllFirst:()=>jW,raceFirst:()=>Xc,reduce:()=>KH,repeat:()=>SJ,repeatOrElse:()=>bJ,replicate:()=>EJ,replicateEffect:()=>IJ,request:()=>BJ,requestUnsafe:()=>jJ,result:()=>cW,retry:()=>vW,retryOrElse:()=>OW,runCallback:()=>KJ,runCallbackWith:()=>JJ,runFork:()=>Nl,runForkWith:()=>Rn,runPromise:()=>tE,runPromiseExit:()=>ah,runPromiseExitWith:()=>GJ,runPromiseWith:()=>rE,runSync:()=>VJ,runSyncExit:()=>Wo,runSyncExitWith:()=>YJ,runSyncWith:()=>ZJ,sandbox:()=>_W,satisfiesErrorType:()=>bK,satisfiesServicesType:()=>EK,satisfiesSuccessType:()=>SK,schedule:()=>TJ,scheduleFrom:()=>Q_,scope:()=>xi,scoped:()=>oh,scopedWith:()=>sh,service:()=>Yb,serviceOption:()=>Qb,setContext:()=>rJ,sleep:()=>ua,spanAnnotations:()=>PJ,spanLinks:()=>MJ,succeed:()=>g,succeedNone:()=>aa,succeedSome:()=>zb,suspend:()=>$,sync:()=>X,tap:()=>Rr,tapCause:()=>V_,tapCauseFilter:()=>AW,tapCauseIf:()=>CW,tapDefect:()=>Z_,tapError:()=>rh,tapErrorTag:()=>wW,timed:()=>BW,timeout:()=>DW,timeoutOption:()=>LW,timeoutOrElse:()=>$W,tracer:()=>wJ,track:()=>uK,trackDefects:()=>pK,trackDuration:()=>dK,trackErrors:()=>lK,trackSuccesses:()=>fK,transposeOption:()=>aW,try:()=>Qc,tryPromise:()=>Zc,tx:()=>mK,txRetry:()=>gK,undefined:()=>QH,uninterruptible:()=>Ts,uninterruptibleMask:()=>Ho,unwrapReason:()=>xW,updateContext:()=>nJ,updateService:()=>oJ,updateServiceScoped:()=>sJ,useSpan:()=>UJ,validate:()=>GH,void:()=>Q,when:()=>nh,whileLoop:()=>Gn,withErrorReporting:()=>FW,withExecutionPlan:()=>MW,withFiber:()=>cn,withFiberSucceed:()=>oW,withLogSpan:()=>cK,withLogger:()=>sK,withParentSpan:()=>$J,withSpan:()=>DJ,withSpanScoped:()=>LJ,withTracer:()=>CJ,withTracerEnabled:()=>AJ,withTracerTiming:()=>kJ,yieldNow:()=>Rl,yieldNowWith:()=>nW,zip:()=>Hb,zipWith:()=>lW});var ir={};Eo(ir,{asVoid:()=>Wq,asVoidAll:()=>hb,die:()=>lb,fail:()=>bs,failCause:()=>sn,filterCause:()=>$q,filterFailure:()=>Lq,filterSuccess:()=>Uq,filterValue:()=>Dq,findDefect:()=>jq,findError:()=>Bq,findErrorOption:()=>Gq,getCause:()=>Kq,getSuccess:()=>Jq,hasDies:()=>Nq,hasFails:()=>Fq,hasInterrupts:()=>db,interrupt:()=>pb,isExit:()=>hl,isFailure:()=>Uo,isSuccess:()=>Ve,map:()=>zq,mapBoth:()=>Hq,mapError:()=>qq,match:()=>mb,succeed:()=>vr,void:()=>xt});var hl=vf,vr=Ae,sn=Ht,bs=en,lb=Er,pb=Nc,xt=pr;var Ve=Qs,Uo=lS,Fq=Wk,Nq=Hk,db=dS,Uq=Bk,Dq=jk,Lq=zk,$q=ol,Bq=pS,jq=qk,mb=Kk,zq=mS,qq=hS,Hq=xS,Wq=Gk,hb=gS,Jq=Vk,Kq=Zk,Gq=Yk;var Vq=\"~effect/Deferred\";var Zq={[Vq]:{_A:R,_E:R},pipe(){return Z(this,arguments)}},KO=function(){this.resumes=void 0,this.effect=void 0};KO.prototype=Zq;var ui=()=>new KO;var Fm=e=>ht(t=>e.effect?t(e.effect):(e.resumes??=[],e.resumes.push(t),z(()=>{let r=e.resumes;if(r===void 0)return;let n=r.indexOf(t);n>=0&&r.splice(n,1)})));var Yq=l(2,(e,t)=>z(()=>Do(e,t))),Qq=Yq;var Xq=l(2,(e,t)=>Qq(e,Ht(t)));var xb=l(2,(e,t)=>Xq(e,Zs(t)));var Do=(e,t)=>{if(e.effect)return!1;if(e.effect=t,e.resumes){let r=e.resumes;e.resumes=void 0;for(let n=0;n{let r={observers:0,deferred:ui(),scope:kn(),finalizer:n=>ne(()=>--r.observers>0?pe:(e.map.delete(t),Le(r.scope,n)))};return r},QO=(e,t)=>t.state._tag===\"Closed\"?!1:(e.observers++,Lc(t,{},e.finalizer),!0),t_=e=>k(e,e_),rH={[e_]:{_ROut:R,_E:R,_RIn:R},pipe(){return Z(this,arguments)}},r_=e=>{let t=Object.create(rH);return t.build=e,t},gb=e=>r_((t,r)=>{let n=Nt(r);return on(e(t,n),o=>o._tag===\"Failure\"?Le(n,o):pe)}),nH=e=>{let t=gb((r,n)=>r.getOrElseMemoize(t,n,e));return t},Um=class{get[YO](){return YO}parent;constructor(t){this.parent=t}map=new Map;get(t,r){let n=this.map.get(t);return n?(QO(n,r),n.deferred.effect??Fm(n.deferred)):this.parent?.get(t,r)}getOrElseMemoize(t,r,n){return ne(()=>{let o;return qn(ne(()=>{let s=this.get(t,r);if(s)return s;let a=tH(this,t);return QO(a,r)?(o=a.deferred,this.map.set(t,a),n(this,a.scope)):n(this,r)}),s=>{o&&Do(o,s)})})}},gl=()=>new Um,oH=e=>new Um(e);var xl=class e extends lr()(\"effect/Layer/CurrentMemoMap\"){static forkOrCreate(t){let r=Fd(t,e);return r?oH(r):gl()}},yl=l(3,(e,t,r)=>kr(ke(ne(()=>e.build(t,r)),Jt(xl,t)),xl,t));var Lm=l(2,(e,t)=>re(r=>yl(e,xl.forkOrCreate(r.context),t))),n_=function(){return arguments.length===1?e=>Dm(Za(arguments[0],e)):Dm(Za(arguments[0],arguments[1]))},Dm=e=>r_(je(D(e)));var yb=function(){return arguments.length===1?e=>XO(arguments[0],e):XO(arguments[0],arguments[1])},XO=(e,t)=>sH(ke(t,r=>Za(e,r))),sH=e=>nH((t,r)=>Es(e,r));var o_=(e,t,r)=>{let n=Nt(r,\"parallel\");return tn(e,o=>o.build(t,Nt(n,\"sequential\")),{concurrency:e.length}).pipe(ke(o=>aA(...o)))},s_=(...e)=>gb((t,r)=>o_(e,t,r));var aH=(e,t,r)=>gb((n,o)=>V(Array.isArray(t)?o_(t,n,o):t.build(n,o),s=>e.build(n,o).pipe(rn(s),ke(a=>r(a,s))))),a_=l(2,(e,t)=>aH(e,t,R));var Hc=Nr;var $m=\"~effect/DateTime\",Bm=\"~effect/DateTime/TimeZone\",c_={[$m]:$m,pipe(){return Z(this,arguments)},[Te](){return this.toString()},toJSON(){return Il(this).toJSON()}},cH={...c_,_tag:\"Utc\",[se](){return It(this.epochMilliseconds)},[ae](e){return Jc(e)&&e._tag===\"Utc\"&&this.epochMilliseconds===e.epochMilliseconds},toString(){return`DateTime.Utc(${Il(this).toJSON()})`}},uH={...c_,_tag:\"Zoned\",[se](){return le(It(this.epochMilliseconds))(H(this.zone))},[ae](e){return Jc(e)&&e._tag===\"Zoned\"&&this.epochMilliseconds===e.epochMilliseconds&&J(this.zone,e.zone)},toString(){return`DateTime.Zoned(${_b(this)})`}},u_={[Bm]:Bm,[Te](){return this.toString()}},fH={...u_,_tag:\"Named\",[se](){return Re(`Named:${this.id}`)},[ae](e){return li(e)&&e._tag===\"Named\"&&this.id===e.id},toString(){return`TimeZone.Named(${this.id})`},toJSON(){return{_id:\"TimeZone\",_tag:\"Named\",id:this.id}}},lH={...u_,_tag:\"Offset\",[se](){return Re(`Offset:${this.offset}`)},[ae](e){return li(e)&&e._tag===\"Offset\"&&this.offset===e.offset},toString(){return`TimeZone.Offset(${kb(this.offset)})`},toJSON(){return{_id:\"TimeZone\",_tag:\"Offset\",offset:this.offset}}},$o=(e,t,r)=>{let n=Object.create(uH);return n.epochMilliseconds=e,n.zone=t,Object.defineProperty(n,\"partsUtc\",{value:r,enumerable:!1,writable:!0}),Object.defineProperty(n,\"adjustedEpochMillis\",{value:void 0,enumerable:!1,writable:!0}),Object.defineProperty(n,\"partsAdjusted\",{value:void 0,enumerable:!1,writable:!0}),n},Jc=e=>k(e,$m);var li=e=>k(e,Bm),f_=e=>li(e)&&e._tag===\"Offset\",l_=e=>li(e)&&e._tag===\"Named\",p_=e=>e._tag===\"Utc\",Eb=e=>e._tag===\"Zoned\",d_=nr((e,t)=>e.epochMilliseconds===t.epochMilliseconds);var m_=e=>{let t=Object.create(cH);return t.epochMilliseconds=e,Object.defineProperty(t,\"partsUtc\",{value:void 0,enumerable:!1,writable:!0}),t},Wc=e=>{let t=e.getTime();if(Number.isNaN(t))throw new ml(\"Invalid date\");return m_(t)},zm=e=>{if(Jc(e))return e;if(e instanceof Date)return Wc(e);if(typeof e==\"object\"){if(\"epochMilliseconds\"in e)return Wc(new Date(e.epochMilliseconds));let t=new Date(0);return SH(t,e),Wc(t)}else if(typeof e==\"string\"&&!pH(e))return Wc(new Date(e+\"Z\"));return Wc(new Date(e))},pH=e=>/Z|GMT|[+-]\\d{2}$|[+-]\\d{2}:?\\d{2}$|\\]$/.test(e),dH=-864e13+720*60*1e3,mH=864e13-840*60*1e3,Ib=(e,t)=>{let r=t?.timeZone;if(r===void 0&&Jc(e)&&Eb(e))return e;let n=zm(e);if(n.epochMillisecondsmH)throw new RangeError(`Epoch millis out of range: ${n.epochMilliseconds}`);r===void 0&&typeof e==\"object\"&&\"timeZoneId\"in e&&(r=e.timeZoneId);let o;if(r===void 0){let s=new Date(n.epochMilliseconds).getTimezoneOffset()*-60*1e3;o=El(s)}else if(li(r))o=r;else if(typeof r==\"number\")o=El(r);else{let s=Cb(r);if(fe(s))throw new ml(`Invalid time zone: ${r}`);o=s.value}return t?.adjustForTimeZone!==!0?$o(n.epochMilliseconds,o,n.partsUtc):bH(n.epochMilliseconds,o,t?.disambiguation??\"compatible\")},Sb=zn(Ib),h_=zn(zm),hH=/^(.{17,35})\\[(.+)\\]$/,x_=e=>{let t=hH.exec(e);if(t===null){let o=Ob(e);return o!==null?Sb(e,{timeZone:o}):v()}let[,r,n]=t;return Sb(r,{timeZone:n})};var g_=e=>m_(e.epochMilliseconds);var jm=new Map,xH={day:\"numeric\",month:\"numeric\",year:\"numeric\",hour:\"numeric\",minute:\"numeric\",second:\"numeric\",timeZoneName:\"longOffset\",fractionalSecondDigits:3,hourCycle:\"h23\"},gH=e=>{let t=e.resolvedOptions().timeZone,r=jm.get(t);if(r!==void 0)return r;let n=Object.create(fH);return n.id=t,n.format=e,jm.set(t,n),n},Tb=e=>{let t=jm.get(e);if(t!==void 0)return t;try{let r=gH(new Intl.DateTimeFormat(\"en-US\",{...xH,timeZone:e}));return jm.set(e,r),r}catch{throw new ml(`Invalid time zone: ${e}`)}},El=e=>{let t=Object.create(lH);return t.offset=e,t},wb=zn(Tb);var yH=/^(?:GMT|[+-])/,Cb=e=>{if(yH.test(e)){let t=Ob(e);return t===null?v():O(El(t))}return wb(e)},y_=e=>e._tag===\"Offset\"?kb(e.offset):e.id;var Il=e=>new Date(e.epochMilliseconds),qm=e=>{if(e._tag===\"Utc\")return new Date(e.epochMilliseconds);if(e.zone._tag===\"Offset\")return new Date(e.epochMilliseconds+e.zone.offset);if(e.adjustedEpochMilliseconds!==void 0)return new Date(e.adjustedEpochMilliseconds);let t=e.zone.format.formatToParts(e.epochMilliseconds).filter(n=>n.type!==\"literal\"),r=new Date(0);return r.setUTCFullYear(Number(t[2].value),Number(t[0].value)-1,Number(t[1].value)),r.setUTCHours(Number(t[3].value),Number(t[4].value),Number(t[5].value),t[6]?.type===\"fractionalSecond\"?Number(t[6].value):(e.epochMilliseconds%1e3+1e3)%1e3),e.adjustedEpochMilliseconds=r.getTime(),r},Ab=e=>qm(e).getTime()-vb(e),kb=e=>{let t=Math.abs(e),r=Math.floor(t/(3600*1e3)),n=Math.round(t%(3600*1e3)/(60*1e3));return n===60&&(r+=1,n=0),`${e<0?\"-\":\"+\"}${String(r).padStart(2,\"0\")}:${String(n).padStart(2,\"0\")}`},S_=e=>kb(Ab(e)),vb=e=>e.epochMilliseconds;var SH=(e,t)=>{if((t.year!==void 0||t.month!==void 0||t.day!==void 0)&&e.setUTCFullYear(t.year??e.getUTCFullYear(),t.month!==void 0?t.month-1:e.getUTCMonth(),t.day??e.getUTCDate()),t.weekDay!==void 0){let r=t.weekDay-e.getUTCDay();e.setUTCDate(e.getUTCDate()+r)}t.hour!==void 0&&e.setUTCHours(t.hour),t.minute!==void 0&&e.setUTCMinutes(t.minute),t.second!==void 0&&e.setUTCSeconds(t.second),t.millisecond!==void 0&&e.setUTCMilliseconds(t.millisecond)};var i_=1440*60*1e3,bH=(e,t,r)=>{if(t._tag===\"Offset\")return $o(e-t.offset,t);let n=bl(e-i_,e,t),o=bl(e+i_,e,t);if(n===o)return $o(e-n,t);let s=n{let t=EH.exec(e);if(t===null)return null;let[,r,n,o]=t;return(r===\"+\"?1:-1)*(Number(n)*60+Number(o))*60*1e3},bl=(e,t,r)=>{let n=r.format.formatToParts(e).find(s=>s.type===\"timeZoneName\")?.value??\"\";if(n===\"GMT\")return 0;let o=Ob(n);return o===null?Ab($o(t,r)):o};var b_=e=>Il(e).toISOString();var bb=e=>{let t=qm(e);return e._tag===\"Utc\"?t.toISOString():`${t.toISOString().slice(0,-1)}${S_(e)}`},_b=e=>e.zone._tag===\"Offset\"?bb(e):`${bb(e)}[${e.zone.id}]`;var zre=globalThis.Number;var I_=l(2,(e,t)=>{if(!globalThis.Number.isFinite(e)||!globalThis.Number.isFinite(t)||t===0)return NaN;if(globalThis.Number.isInteger(e)&&globalThis.Number.isInteger(t))return e%t;let[r,n]=E_(e),[o,s]=E_(t),a=Math.min(n,s),i=r*BigInt(10)**BigInt(n-a),c=o*BigInt(10)**BigInt(s-a),u=i%c;if(u===BigInt(0))return e<0||Object.is(e,-0)?-0:0;let f=globalThis.Number(`${u}e${a}`);return f===0?Math.sign(e)*globalThis.Number.MIN_VALUE:f});function E_(e){if(globalThis.Number.isInteger(e))return[BigInt(e),0];let t=Math.abs(e).toExponential(),r=t.indexOf(\"e\"),n=t.slice(0,r).replace(\".\",\"\");return[BigInt(n)*(e<0?-BigInt(1):BigInt(1)),globalThis.Number(t.slice(r+1))-n.length+1]}var T_=e=>{let t=Math.ceil(Math.log(e)/Math.log(2));return Math.max(Math.pow(2,t),2)};var Hre=globalThis.String,Hm=br;var w_=e=>e.toUpperCase();var C_=e=>e.trim();var wH=(e,t,r,n,o)=>{let s=e;for(let c of t)s=s.replace(c,\"$1\\0$2\");for(let c of r)s=s.replace(c,\"\\0\");let a=0,i=s.length;for(;s.charAt(a)===\"\\0\";)a++;for(;s.charAt(i-1)===\"\\0\";)i--;return s.slice(a,i).split(\"\\0\").map(o).join(n)};var CH=[/([a-z0-9])([A-Z])/g,/([A-Z])([A-Z][a-z])/g],AH=/[^A-Z0-9]+/gi;var A_=e=>wH(e,CH,[AH],\"_\",w_);var He=l(2,(e,t)=>xs(e,kH,r=>t(r))),Bo=e=>e.reasons.some(Rb),Rb=e=>e._tag===\"Fail\"&&qc(e.error),Tl=e=>{let t,r=!1;for(let n of e.reasons)Rb(n)?t??=n.error:n._tag!==\"Interrupt\"&&(r=!0);return t===void 0?W(e):r?W(oa(e.reasons.filter(n=>!Rb(n)))):B(t)};var kH=e=>{let t=Tl(e);return ce(t)?t:B(t.success.value)},Jm=e=>{let t=Tl(e);return ce(t)?sn(t.failure):vr(t.success.value)},Km=l(2,(e,t)=>Mo(e,{onSuccess:t.onSuccess,onFailure:r=>{let n=Tl(r);return ce(n)?t.onFailure(n.failure):t.onDone(n.success.value)}}));var k_=\"~effect/Schedule\";var Gt=we(\"effect/Schedule/CurrentMetadata\",{defaultValue:je({input:void 0,output:void 0,duration:Ws,attempt:0,start:0,now:0,elapsed:0,elapsedSincePrevious:0})}),vH={[k_]:{_Out:R,_In:R,_Env:R},pipe(){return Z(this,arguments)}},Pb=e=>k(e,k_),wl=e=>{let t=Object.create(vH);return t.step=e,t},Mb=()=>{let e=0,t,r;return(n,o)=>{r===void 0&&(r=n);let s=n-r,a=t===void 0?0:n-t;return t=n,{input:o,attempt:++e,start:r,now:n,elapsed:s,elapsedSincePrevious:a}}},OH=e=>wl(ke(e,t=>{let r=Mb();return(n,o)=>t(r(n,o))})),di=e=>nn(e.step,t=>D(()=>De(t))),Kn=e=>Wn(t=>ke(di(e),r=>{let n=Mb();return o=>ne(()=>{let s=t.currentTimeMillisUnsafe();return V(r(s,o),([a,i])=>{let c=n(s,o);return c.output=a,c.duration=i,Ar(Bc(i),c)})})})),Cl=e=>ke(Kn(e),t=>r=>ke(t(r),n=>n.output));var Fb=e=>wl(ke(di(e),t=>(r,n)=>Km(t(r,n),{onSuccess:o=>D([n,o[1]]),onFailure:De,onDone:()=>Y(n)}))),v_=e=>Al(kl,({attempt:t})=>D(t<=e)),Nb=e=>{let t=Ge(e);return OH(D(r=>D([r.attempt-1,t])))};var Al=l(2,(e,t)=>wl(ke(di(e),r=>{let n=Mb();return(o,s)=>V(r(o,s),a=>{let[i,c]=a,u=t({...n(o,s),output:i,duration:c});return V(ie(u)?u:D(u),f=>f?D(a):Y(i))})})));var kl=Nb(Ws);var O_=\"~effect/ExecutionPlan\";var _H={[O_]:O_,get captureRequirements(){let e=this;return Xs(t=>D(RH(e.steps.map(r=>({...r,while:r.while?n=>rn(r.while(n),t):void 0,schedule:r.schedule?wl(rn(ke(di(r.schedule),n=>(o,s)=>rn(n(o,s),t)),t)):void 0,provide:t_(r.provide)?a_(r.provide,Dm(t)):r.provide})))))},pipe(){return Z(this,arguments)}},RH=e=>{let t=Object.create(_H);return t.steps=e,t};var Gm=we(\"effect/ExecutionPlan/CurrentMetadata\",{defaultValue:je({attempt:0,stepIndex:0})});var PH=(e,t,r)=>Cm(n=>V(r?.local?yl(t,gl(),n):Lm(t,n),o=>rn(e,o))),Vm=l(e=>ie(e[0]),(e,t,r)=>Uf(t)?rn(e,t):PH(e,Array.isArray(t)?s_(...t):t,r));var R_=e=>e.reasons.every(jn)?Ur(e):W(e),Db=l(3,(e,t,r)=>V(Kn(t),n=>{let o=Gt.defaultValue();return xs(He(ym(si(V(ne(()=>kr(e,Gt,o)),n),s=>z(()=>{o=s})),{disableYield:!0}),s=>D(s)),R_,s=>r(s,o.attempt===0?v():O(o)))})),Lb=l(3,(e,t,r)=>V(Kn(t),n=>{let o=Gt.defaultValue(),s,a=xs(ne(()=>kr(e,Gt,o)),R_,i=>(s=i,V(n(i),c=>(o=c,a))));return He(a,i=>js(()=>r(s,i)))})),P_=l(2,(e,t)=>{let r=typeof t==\"function\"?t(R):Pb(t)?t:Ol(t);return Db(e,r,Qe)}),vl=l(2,(e,t)=>{let r=typeof t==\"function\"?t(R):Pb(t)?t:Ol(t);return Lb(e,r,Qe)}),M_=l(3,(e,t,r)=>V(Kn(r),n=>{let o=Gt.defaultValue(),s=ne(()=>kr(e,Gt,o));return He(V(n(t),a=>{o=a;let i=je(V(s,n));return No({while:Sr,body:i,step(c){o=c}})}),a=>D(a))})),MH=Fb(kl),Ol=e=>{let t=e.schedule?Fb(e.schedule):MH;return e.while&&(t=Al(t,({input:r})=>{let n=e.while(r);return ie(n)?n:D(n)})),e.until&&(t=Al(t,({input:r})=>{let n=e.until(r);return ie(n)?ke(n,o=>!o):D(!n)})),e.times!==void 0&&(t=Al(t,({attempt:r})=>D(r<=e.times))),t};var $b=(e,t)=>{let r=-1,n=0,o=s=>Em(e(s));return{begin:Wn(s=>ne(()=>{let a=t();a.stepIndex!==r&&(r=a.stepIndex,n=0),n++;let i={attempt:a.attempt,stepAttempt:n,stepIndex:a.stepIndex,startNanos:s.monotonicTimeNanosUnsafe()};return Ar(o({_tag:\"AttemptStart\",attempt:i.attempt,stepAttempt:i.stepAttempt,stepIndex:i.stepIndex}),i)})),end:(s,a)=>Wn(i=>{let c=Ro(i.monotonicTimeNanosUnsafe()-s.startNanos);return o(a._tag===\"Success\"?{_tag:\"AttemptSuccess\",attempt:s.attempt,stepAttempt:s.stepAttempt,stepIndex:s.stepIndex,duration:c}:{_tag:\"AttemptFailure\",attempt:s.attempt,stepAttempt:s.stepAttempt,stepIndex:s.stepIndex,duration:c,cause:a.cause})})}},N_=l(e=>ie(e[0]),(e,t,r)=>ne(()=>{let n=0,o={attempt:0,stepIndex:0},s=gm(Gm,z(()=>(o={attempt:o.attempt+1,stepIndex:n},o))),a=r?.onEvent===void 0?void 0:$b(r.onEvent,()=>o),i=a===void 0?R:u=>Hn(f=>V(a.begin,p=>on(f(u),d=>a.end(p,d)))),c;return V(No({while:()=>np?d:(p=!0,Pc(c))),f=vl(f,_l(u,!1))}else{let p=_l(u,!0);f=p?vl(f,p):f}return Vs(f)},step(u){c=u,n++}}),()=>Pc(c))})),_l=(e,t)=>{if(t){if(e.attempts===1||!(e.schedule||e.attempts))return}else return Ol({schedule:e.schedule?e.schedule:e.attempts?void 0:FH,times:e.attempts,while:e.while});return Ol({schedule:e.schedule,while:e.while,times:e.attempts?e.attempts-1:void 0})},FH=v_(1);var NH=\"~effect/Request\",UH=gC({_E:e=>e,_A:e=>e,_R:e=>e}),cne={...wC,[NH]:UH};var D_=e=>e;var $_=l(2,(e,t)=>{let r=n=>ht(o=>{let s=j_(n,e,o,ds());return LH(n,s)});return ie(t)?V(t,r):r(t)}),B_=(e,t)=>{let r=j_(t.resolver,e,t.onExit,{context:t.context,cache:{scheduler:Fe(t.context,zf)}});return()=>z_(t.resolver,r)},Zm=[],Bb=new WeakMap,j_=(e,t,r,n)=>{let o=Bb.get(e);o||(o=new Map,Bb.set(e,o));let s,a=!1,i=D_({request:t,context:n.context,uninterruptible:!1,completeUnsafe(u){a||(a=!0,!(s&&!s.entrySet.delete(i))&&r(u))}});if(e.preCheck!==void 0&&!e.preCheck(i))return i;let c=e.batchKey(i);if(s=o.get(c),!s){if(Zm.length>0)s=Zm.pop(),s.key=c,s.resolver=e,s.map=o;else{let u={key:c,resolver:e,map:o,entrySet:new Set,entries:new Set,delayEffect:V(ne(()=>u.resolver.delay),f=>L_(u)),run:on(ne(()=>u.resolver.runAll(Array.from(u.entries),u.key)),f=>{for(let p of u.entrySet)p.completeUnsafe(f._tag===\"Success\"?Er(new Error(\"Effect.request: RequestResolver did not complete request\",{cause:p.request})):f);return u.entries.clear(),Zm.length<128&&(u.entrySet.clear(),u.key=void 0,u.fiber=void 0,u.resolver=void 0,u.map=void 0,Zm.push(u)),pe})};s=u}o.set(c,s),s.fiber=ys(n.context)(s.delayEffect,{scheduler:n.cache.scheduler})}return s.entrySet.add(i),s.entries.add(i),s.resolver.collectWhile(s.entries)||(s.fiber.interruptUnsafe(n.id),s.fiber=ys(n.context)(L_(s),{scheduler:n.cache.scheduler})),i},z_=(e,t)=>{if(t.uninterruptible)return;let r=Bb.get(e);if(!r)return;let n=e.batchKey(t),o=r.get(n);if(!o||!o.entries.delete(t))return;o.entrySet.delete(t);let s;o.entries.size===0&&(r.delete(n),s=o.fiber),t.completeUnsafe(Nc()),s?.interruptUnsafe()},LH=(e,t)=>z(()=>z_(e,t));function L_(e){return e.map.has(e.key)?(e.map.delete(e.key),e.run):pe}var BH=\"effect/Metric/CurrentMetricAttributes\",jH=we(BH,{defaultValue:()=>({})}),zH=\"effect/Metric/MetricRegistry\",qH=we(zH,{defaultValue:()=>new Map}),q_=\"~effect/Metric\",H_=class{[q_]=q_;#t;#e=\"\";id;description;attributes;constructor(t,r,n){this.id=t,this.description=r,this.attributes=n}valueUnsafe(t){return this.hook(t).get(t)}modifyUnsafe(t,r){return this.hook(r).modify(t,r)}updateUnsafe(t,r){return this.hook(r).update(t,r)}hook(t){let r=Fe(t,jH);r!==this.#t&&(this.#t=r,this.#e=HH(this,W_(this.attributes,r)));let n=Fe(t,qH),o=n.get(this.#e);return aC(o)&&(o={id:this.id,type:this.type,description:this.description,attributes:Object.keys(r).length>0?W_(this.attributes,r):this.attributes,hooks:this.createHooks()},n.set(this.#e,o)),o.hooks}pipe(){return Z(this,arguments)}};var Gc=l(2,(e,t)=>Xs(r=>z(()=>e.updateUnsafe(t,r))));function HH(e,t){let r=Object.entries(t).sort((n,o)=>n[0]o[0]?1:0);return JSON.stringify([e.type,e.id,e.description,r])}function W_(e,t){return{...J_(e),...J_(t)}}function J_(e){return Io(e)&&Array.isArray(e)?e.reduce((t,[r,n])=>(M(t,r,n),t),{}):e}var WH=ko,jo=ie,Ym=ll,JH=$S,KH=Kv,GH=Gv,VH=Vv,ZH=Yv,sa=tn,YH=Xv,Gn=No,Qm=nS,Zc=Ek,g=D,aa=oi,zb=rS,$=ne,X=z,Q=pe;var QH=YS;var Yc=ht,mi=mm,XH=sO,eW=aO,tW=cO;var rW=iO,dr=Ck,P=Qe,Xm=dm,ze=De,Is=Sk,an=Ys,Qc=bk;var Rl=Mc,nW=tS,cn=re,oW=kC,Pl=Pc,sW=eS,aW=yk,iW=gk,I=V,qb=Nk,_r=Fo,Rr=si,cW=Vs,uW=bv,vn=il,j=ke,Tt=Ar,eh=Fc,ia=nl,fW=_k,Hb=rv,lW=SS,ca=gs;var pW=TS,dW=dv,mW=mv,hW=hv,xW=xv,rt=nn,Wb=cv,gW=sl,yW=bm,SW=iv,bW=ES,EW=xs,th=wS,IW=CS,TW=AS,rh=IS,wW=lv,V_=uv,CW=fv,AW=Sm,Z_=pv,kW=kS,vW=vl,OW=Lb,_W=Rk,RW=Sv,PW=Em,MW=N_,FW=JO,NW=gv,UW=yv,DW=kv,LW=_S,$W=OS,Jb=Av,ua=Bc,BW=vv,Kb=cS,jW=uS,zW=Pk,Xc=fS,Gb=rO,qW=nO,Vb=oO,HW=BS,WW=jS,JW=nv,KW=tO,nh=ov,Zb=Im,GW=al,VW=vS,ZW=Tv,YW=Mk,zo=Mo,QW=Dc,XW=wv,eJ=Cv,hi=yS,Ml=Xs,tJ=Vm,On=rn,rJ=tv,Yb=Qk,Qb=Xk,nJ=Uc,oJ=hs,sJ=ev,qo=kr,aJ=gm,xi=cl,oh=PS,sh=Cm,gi=ai,iJ=Lv,Y_=ul,cJ=MS,yi=Nv,Vn=US,uJ=Uv,fJ=DS,Fl=qn,un=on,lJ=FS,pJ=NS,dJ=jv,mJ=Bv,hJ=LS,_n=zv,xJ=Am,Xb=Dv,Ts=ii,Ho=Hn,gJ=Wv,yJ=Jv,$t=ym,SJ=P_,bJ=Db,EJ=bS,IJ=sv,TJ=l(2,(e,t)=>Q_(e,void 0,t)),Q_=M_,wJ=EO,CJ=IO,AJ=TO,kJ=wO,vJ=PO,OJ=MO,_J=FO,RJ=NO,PJ=vO,MJ=OO,FJ=_O,eE=AO,NJ=Jf,UJ=Kf,DJ=RO,LJ=kO,$J=_c,BJ=$_,jJ=B_,eu=uO,Ut=zS,tu=pO,zJ=fO,qJ=lO,HJ=Tk,WJ=wk,Nl=mO,Rn=ys,JJ=qS,KJ=hO,tE=gO,rE=HS,ah=xO,GJ=Om,VJ=SO,ZJ=WS,Wo=yO,YJ=_m,Se=sS,QJ=Ak,Si=Wn,ih=Jn,XJ=Jn(),eK=Jn(\"Fatal\"),tK=Jn(\"Warn\"),nE=Jn(\"Error\"),rK=Jn(\"Info\"),nK=Jn(\"Debug\"),oK=Jn(\"Trace\"),sK=l(2,(e,t)=>hs(e,ZS,r=>new Set([...r,t]))),aK=l(e=>jo(e[0]),(e,...t)=>hs(e,GO,r=>{let n=t.length===1?{...r,...t[0]}:{...r};return t.length===1||M(n,t[0],t[1]),n})),iK=HO,cK=l(2,(e,t)=>V(GS,r=>hs(e,VO,n=>[[t,r],...n]))),uK=l(e=>jo(e[0]),(e,t,r)=>un(e,n=>{let o=r===void 0?n:r(n);return Gc(t,o)})),fK=l(e=>jo(e[0]),(e,t,r)=>Rr(e,n=>{let o=r===void 0?n:r(n);return Gc(t,o)})),lK=l(e=>jo(e[0]),(e,t,r)=>rh(e,n=>{let o=r===void 0?n:r(n);return Gc(t,o)})),pK=l(e=>jo(e[0]),(e,t,r)=>Z_(e,n=>{let o=r===void 0?n:r(n);return Gc(t,o)})),dK=l(e=>jo(e[0]),(e,t,r)=>Si(n=>{let o=n.monotonicTimeNanosUnsafe();return un(e,()=>{let s=n.monotonicTimeNanosUnsafe(),a=PA(Ge(s),Ge(o)),i=r===void 0?a:r(a);return Gc(t,i)})})),Vc=class extends lr()(\"effect/Effect/Transaction\"){},mK=e=>cn(t=>{let r=Fd(t.context,Vc);if(r&&!G_.has(r))return e;r={journal:new Map,retry:!1};let n;return Ho(o=>I(Gn({while:()=>!n,body:je(o(e).pipe(qo(Vc,r),V_(s=>r.retry?cm(s)?o(hK(r)):X(()=>jb(r)):Q),vn)),step(s){if(r.retry||!X_(r))return jb(r);Ve(s)&&xK(t,r),jb(r),G_.add(r),n=s}}),()=>n))}),G_=new WeakSet,X_=e=>{for(let[t,{version:r}]of e.journal)if(t.version!==r)return!1;return!0},hK=e=>Yc(t=>{if(!X_(e))return t(Q);let r={},n=Array.from(e.journal.keys()),o=()=>{for(let a of n)a.pending.delete(r)},s=()=>{o(),t(Q)};for(let a of n)a.pending.set(r,s);return X(o)});function xK(e,t){for(let[r,{value:n,written:o}]of t.journal)if(o){Object.is(n,r.value)||(r.version=r.version+1,r.value=n);for(let s of r.pending.values())e.currentDispatcher.scheduleTask(s,0);r.pending.clear()}}function jb(e){e.retry=!1,e.journal.clear()}var gK=I(Vc,e=>(e.retry=!0,_n)),yK=(e,t,r)=>(...n)=>Yc(o=>{try{e(...n,(s,a)=>{o(s?t?$(()=>P(t(s,n))):P(s):g(a))})}catch(s){o(r?P(r(s,n)):an(s))}}),SK=()=>e=>e,bK=()=>e=>e,EK=()=>e=>e,Dr=Uk,Ul=Dk,Vt=Lk,st=Fk,ru=$k,oE=kk;var eR=/^[+-]?\\d+$/,aE=\"~effect/BigDecimal\",IK={[aE]:aE,[se](){let e=cE(this);return le(Re(String(e.value)),It(e.scale))},[ae](e){return iE(e)&&aR(this,e)===0},toString(){return`BigDecimal(${ws(this)})`},toJSON(){return{_id:\"BigDecimal\",value:String(this.value),scale:this.scale}},[Te](){return this.toJSON()},pipe(){return Z(this,arguments)}},iE=e=>k(e,aE),nR=(e,t)=>{if(!Number.isSafeInteger(t))throw new RangeError(`Scale must be a safe integer, got ${t}`);let r=Object.create(IK);return r.value=e,r.scale=t,r},oR=(e,t)=>{let r=nR(e,t);return r.normalized=r,r};var Zn=BigInt(0),TK=BigInt(1);var wK=BigInt(10),sR=oR(Zn,0);var cE=e=>{if(e.normalized===void 0)if(e.value===Zn)e.normalized=sR;else{let t=`${e.value}`,r=t.length;for(;t[r-1]===\"0\";)r--;e.normalized=oR(BigInt(t.slice(0,r)),e.scale-(t.length-r))}return e.normalized};var CK=100,AK=[TK],sE=(e,t)=>e===t?0:e{let r=`${e.value{if(e.scale===t.scale)return sE(e.value,t.value);let r=rR(e),n=rR(t);if(r!==n)return rCK)return r===-1?tR(t,e):tR(e,t);let a=AK[s]??=wK**BigInt(s);return o>0?sE(e.value,t.value*a):sE(e.value*a,t.value)},nu=zs(aR);var rR=e=>e.value===Zn?0:e.value{if(e===\"\")return O(sR);let t,r,n=e.search(/[eE]/);if(n!==-1){let c=e.slice(n+1);if(t=e.slice(0,n),r=Number(c),t===\"\"||!Number.isSafeInteger(r)||!eR.test(c))return v()}else t=e,r=0;let o,s,a=t.search(/\\./);if(a!==-1){let c=t.slice(0,a),u=t.slice(a+1);o=`${c}${u}`,s=u.length}else o=t,s=0;if(!eR.test(o))return v();let i=s-r;return Number.isSafeInteger(i)?O(nR(BigInt(o),i)):v()};var ws=e=>{let t=cE(e);if(Math.abs(t.scale)>=16)return kK(t);let r=t.value0?n.padStart(t.scale+1,\"0\"):n.padEnd(n.length-t.scale,\"0\"),s=o.length-t.scale,a=t.scale>0?`${o.slice(0,s)}.${o.slice(s)}`:o;return r?`-${a}`:a},kK=e=>{if(vK(e))return\"0e+0\";let t=cE(e),r=`${t.value}`,n=t.value=0?\"+\":\"\"}${a}`};var vK=e=>e.value===Zn;var me={};Eo(me,{Any:()=>tV,Array:()=>$e,ArrayEnsure:()=>pV,BigDecimal:()=>pD,BigDecimalFromString:()=>JZ,BigInt:()=>Bu,BigIntFromString:()=>_Z,Boolean:()=>XT,BooleanFromBit:()=>DZ,BooleanLiterals:()=>nw,ByteSize:()=>Ju,ByteSizeFromBigInt:()=>GZ,ByteSizeFromNumber:()=>VZ,ByteSizeFromString:()=>KZ,Cause:()=>VT,CauseReason:()=>GT,Char:()=>hZ,Chunk:()=>ZZ,Class:()=>zD,Cookie:()=>$D,Cookies:()=>BD,Date:()=>Wu,DateFromMillis:()=>TZ,DateFromString:()=>IZ,DateTimeUtc:()=>Rx,DateTimeUtcFromDate:()=>r5,DateTimeUtcFromMillis:()=>o5,DateTimeUtcFromString:()=>n5,DateTimeZoned:()=>OD,DateTimeZonedFromString:()=>c5,Defect:()=>gZ,Duration:()=>Mx,DurationFromMillis:()=>Y5,DurationFromNanos:()=>Z5,DurationFromString:()=>hw,Enum:()=>X3,Error:()=>qD,ErrorInstance:()=>xZ,Exit:()=>Q5,FalseLiterals:()=>jU,File:()=>sD,Finite:()=>qu,FiniteFromString:()=>OZ,FormData:()=>aD,Graph:()=>r8,HashMap:()=>n8,HashSet:()=>o8,Headers:()=>u8,InetAddress:()=>DD,InetAddressFromString:()=>q5,InetAddressV4:()=>j5,InetAddressV6:()=>z5,Int:()=>lo,IpAddress:()=>Ki,IpAddressFromString:()=>Gu,IpInterface:()=>MD,IpInterfaceFromString:()=>D5,IpLinkLocalAddress:()=>T5,IpLinkLocalAddressFromString:()=>w5,IpLoopbackAddress:()=>E5,IpLoopbackAddressFromString:()=>I5,IpMulticastAddress:()=>u5,IpMulticastAddressFromString:()=>f5,IpNetwork:()=>UD,IpNetworkFromString:()=>B5,IpUnicastAddress:()=>d5,IpUnicastAddressFromString:()=>m5,IpUnspecifiedAddress:()=>C5,IpUnspecifiedAddressFromString:()=>A5,Ipv4Address:()=>Px,Ipv4AddressFromString:()=>dw,Ipv4BroadcastAddress:()=>g5,Ipv4BroadcastAddressFromString:()=>y5,Ipv4Interface:()=>RD,Ipv4InterfaceFromString:()=>N5,Ipv4Network:()=>FD,Ipv4NetworkFromString:()=>L5,Ipv4PrivateAddress:()=>k5,Ipv4PrivateAddressFromString:()=>v5,Ipv6Address:()=>mw,Ipv6AddressFromString:()=>_D,Ipv6Interface:()=>PD,Ipv6InterfaceFromString:()=>U5,Ipv6Network:()=>ND,Ipv6NetworkFromString:()=>$5,Ipv6UniqueLocalAddress:()=>O5,Ipv6UniqueLocalAddressFromString:()=>_5,Json:()=>Nx,JsonFromUrlParamsField:()=>f8,JsonObject:()=>L8,Literal:()=>Ct,Literals:()=>fo,MacAddress:()=>Ji,MacAddressFromString:()=>Ku,MacBroadcastAddress:()=>S5,MacBroadcastAddressFromString:()=>b5,MacLocallyAdministeredAddress:()=>R5,MacLocallyAdministeredAddressFromString:()=>P5,MacMulticastAddress:()=>l5,MacMulticastAddressFromString:()=>p5,MacUnicastAddress:()=>h5,MacUnicastAddressFromString:()=>x5,MacUniversallyAdministeredAddress:()=>M5,MacUniversallyAdministeredAddressFromString:()=>F5,MutableJson:()=>B8,Natural:()=>zi,Never:()=>eV,NonEmptyArray:()=>tw,NonEmptyString:()=>fw,Null:()=>wx,NullOr:()=>rw,NullishOr:()=>PU,Number:()=>Wi,NumberFromString:()=>vZ,ObjectKeyword:()=>oV,Opaque:()=>CV,Option:()=>Gi,OptionFromNullOr:()=>s8,OptionFromNullishOr:()=>i8,OptionFromOptional:()=>d8,OptionFromOptionalKey:()=>p8,OptionFromOptionalNullOr:()=>m8,OptionFromUndefinedOr:()=>a8,PropertyKey:()=>KT,ReadonlyMap:()=>yZ,ReadonlySet:()=>SZ,Record:()=>_s,RecordFromCookies:()=>c8,RecordFromUrlParams:()=>l8,Redacted:()=>Fx,RedactedFromValue:()=>h8,RegExp:()=>bZ,Result:()=>x8,SchemaError:()=>Os,SocketAddress:()=>W5,StandardSchemaV1FailureResult:()=>UZ,String:()=>K,StringForLiteralAutocomplete:()=>rV,StringFromBase64:()=>PZ,StringFromBase64Url:()=>MZ,StringFromHex:()=>FZ,StringFromUriComponent:()=>NZ,Struct:()=>Me,StructWithRest:()=>fV,Symbol:()=>OU,TaggedClass:()=>b8,TaggedError:()=>E8,TaggedStruct:()=>kx,TaggedUnion:()=>wV,TemplateLiteral:()=>Y3,TemplateLiteralParser:()=>Q3,TimeZone:()=>kD,TimeZoneFromString:()=>i5,TimeZoneNamed:()=>CD,TimeZoneNamedFromString:()=>a5,TimeZoneOffset:()=>s5,Tree:()=>D8,Trim:()=>RZ,Trimmed:()=>cD,TrueLiterals:()=>BU,Tuple:()=>Qo,TupleWithRest:()=>lV,URL:()=>Ox,URLFromString:()=>EZ,URLSearchParams:()=>iD,Uint8Array:()=>_x,Uint8ArrayFromBase64:()=>LZ,Uint8ArrayFromBase64Url:()=>$Z,Uint8ArrayFromHex:()=>BZ,Undefined:()=>QT,UndefinedOr:()=>Ax,Union:()=>it,UniqueArray:()=>dV,UniqueSymbol:()=>sV,UnixPathAddress:()=>LD,UnixPathAddressFromString:()=>H5,Unknown:()=>vp,UnknownFromJsonString:()=>CZ,UrlParams:()=>xw,Void:()=>nV,annotate:()=>w3,annotateEncoded:()=>C3,annotateKey:()=>A3,asserts:()=>R3,brand:()=>NU,catchDecoding:()=>gV,catchDecodingWithContext:()=>LU,catchEncoding:()=>yV,catchEncodingWithContext:()=>$U,check:()=>hV,declare:()=>Be,declareConstructor:()=>qr,decode:()=>SV,decodeEffect:()=>P3,decodeExit:()=>M3,decodeOption:()=>N3,decodePromise:()=>D3,decodeResult:()=>U3,decodeSync:()=>L3,decodeTo:()=>G,decodeUnknownEffect:()=>Ex,decodeUnknownExit:()=>bU,decodeUnknownOption:()=>F3,decodeUnknownPromise:()=>IU,decodeUnknownResult:()=>EU,decodeUnknownSync:()=>TU,encode:()=>bV,encodeEffect:()=>$3,encodeExit:()=>B3,encodeKeys:()=>cV,encodeOption:()=>z3,encodePromise:()=>H3,encodeResult:()=>q3,encodeSync:()=>W3,encodeTo:()=>ow,encodeUnknownEffect:()=>Ix,encodeUnknownExit:()=>wU,encodeUnknownOption:()=>j3,encodeUnknownPromise:()=>AU,encodeUnknownResult:()=>CU,encodeUnknownSync:()=>kU,extendTo:()=>uV,fieldsAssign:()=>aV,flip:()=>JT,fromBrand:()=>xV,fromFormData:()=>AZ,fromJsonString:()=>lw,fromURLSearchParams:()=>kZ,instanceOf:()=>ba,is:()=>_3,isBase64:()=>KU,isBase64Url:()=>FV,isBetween:()=>Hu,isBetweenBigDecimal:()=>WZ,isBetweenBigInt:()=>oZ,isBetweenCodePoints:()=>iZ,isBetweenDate:()=>eZ,isBetweenLength:()=>iw,isBetweenProperties:()=>dZ,isBetweenSize:()=>fZ,isCapitalized:()=>BV,isEndingWith:()=>UV,isFinite:()=>zV,isGUID:()=>PV,isGreaterThan:()=>HV,isGreaterThanBigDecimal:()=>jZ,isGreaterThanBigInt:()=>tZ,isGreaterThanDate:()=>ZV,isGreaterThanOrEqualTo:()=>GU,isGreaterThanOrEqualToBigDecimal:()=>zZ,isGreaterThanOrEqualToBigInt:()=>VU,isGreaterThanOrEqualToDate:()=>YV,isIncluding:()=>DV,isInt:()=>vx,isInt32:()=>GV,isLessThan:()=>WV,isLessThanBigDecimal:()=>qZ,isLessThanBigInt:()=>rZ,isLessThanDate:()=>QV,isLessThanOrEqualTo:()=>JV,isLessThanOrEqualToBigDecimal:()=>HZ,isLessThanOrEqualToBigInt:()=>nZ,isLessThanOrEqualToDate:()=>XV,isLowercased:()=>$V,isMaxCodePoints:()=>aZ,isMaxLength:()=>XU,isMaxProperties:()=>pZ,isMaxSize:()=>uZ,isMinCodePoints:()=>sZ,isMinLength:()=>ZU,isMinProperties:()=>lZ,isMinSize:()=>cZ,isMultipleOf:()=>KV,isNonEmpty:()=>QU,isPattern:()=>zu,isPropertyNames:()=>mZ,isSchema:()=>Tx,isSchemaError:()=>hU,isStartingWith:()=>NV,isStringBigInt:()=>vV,isStringFinite:()=>kV,isStringSymbol:()=>OV,isTrimmed:()=>JU,isULID:()=>MV,isUUID:()=>RV,isUint32:()=>VV,isUncapitalized:()=>jV,isUnique:()=>uw,isUniqueKey:()=>eD,isUppercased:()=>LV,link:()=>Oe,linkDecoding:()=>ju,make:()=>q,makeFilter:()=>Ue,makeFilterGroup:()=>AV,makeIsBetween:()=>Mp,makeIsGreaterThan:()=>Op,makeIsGreaterThanOrEqualTo:()=>_p,makeIsLessThan:()=>Rp,makeIsLessThanOrEqualTo:()=>Pp,makeIsMultipleOf:()=>qV,middlewareDecoding:()=>UU,middlewareEncoding:()=>DU,mutable:()=>mV,mutableKey:()=>G3,optional:()=>Lu,optionalKey:()=>qi,overrideToCodecIso:()=>N8,overrideToEquivalence:()=>w8,overrideToFormatter:()=>I8,readonlyKey:()=>V3,refine:()=>FU,required:()=>K3,requiredKey:()=>J3,resolveAnnotations:()=>j8,resolveAnnotationsKey:()=>z8,revealBottom:()=>T3,revealCodec:()=>k3,suspend:()=>MU,tag:()=>Hi,tagDefaultOmit:()=>TV,toCodecArrayFromSingle:()=>_8,toCodecIso:()=>O8,toCodecJson:()=>v8,toCodecStringTree:()=>Np,toDifferJsonPatch:()=>U8,toEncoded:()=>$u,toEncoderXml:()=>R8,toEquivalence:()=>C8,toFormatter:()=>T8,toIso:()=>P8,toIsoFocus:()=>F8,toIsoSource:()=>M8,toJsonSchemaDocument:()=>k8,toRepresentation:()=>A8,toStandardJSONSchemaV1:()=>O3,toStandardSchemaV1:()=>v3,toTaggedUnion:()=>WU,toType:()=>Hr,withArrayLengthConstraints:()=>Cx,withConstructorDefault:()=>zU,withDecodingDefault:()=>HU,withDecodingDefaultKey:()=>sw,withDecodingDefaultType:()=>IV,withDecodingDefaultTypeKey:()=>EV});var uR=globalThis.BigInt;var fR=e=>e>uR(Number.MAX_SAFE_INTEGER)||ee.names.map(t=>[t,e])));var fE=e=>e;var ou=e=>{throw new Error(`Invalid ByteSize: ${e}`)},dR=e=>!Number.isSafeInteger(e)||e<0?ou(`expected a non-negative safe integer, received ${e}`):fE(BigInt(e));var mR=e=>{let t=/^\\s*(\\d+)(?:\\.(\\d+))?\\s*([A-Za-z]+)\\s*$/.exec(e);if(t===null)return ou(`unsupported syntax ${JSON.stringify(e)}`);let r=PK.get(t[3]);if(r===void 0)return ou(`unsupported unit ${JSON.stringify(t[3])}`);let n=t[2]??\"\",o=BigInt(10)**BigInt(n.length),s=BigInt(t[1]+n)*r.factor;return s%o!==uE?ou(`${JSON.stringify(e)} does not represent an integral number of bytes`):fE(s/o)},hR=zn(mR),ch=e=>{switch(typeof e){case\"bigint\":return etypeof e==\"bigint\"?ch(e):dR(e);var gR=e=>typeof e==\"bigint\"&&e>=uE;var uh=e=>e,yR=fR;var bR=\"~effect/Chunk\";function SR(e,t,r,n,o){for(let s=t;snr((t,r)=>{if(t.length!==r.length)return!1;let n=bi(t),o=bi(r);for(let s=0;se},toString(){return`Chunk(${F(bi(this))})`},toJSON(){return{_id:\"Chunk\",values:vt(bi(this))}},[Te](){return this.toJSON()},[ae](e){return lh(e)&&NK(this,e)},[se](){return To(bi(this))},[Symbol.iterator](){switch(this.backing._tag){case\"IArray\":return this.backing.array[Symbol.iterator]();case\"IEmpty\":return ER[Symbol.iterator]();default:return bi(this)[Symbol.iterator]()}},pipe(){return Z(this,arguments)}},mE=e=>{let t=Object.create(UK);switch(t.backing=e,e._tag){case\"IEmpty\":{t.length=0,t.depth=0,t.left=t,t.right=t;break}case\"IConcat\":{t.length=e.left.length+e.right.length,t.depth=1+Math.max(e.left.depth,e.right.depth),t.left=e.left,t.right=e.right;break}case\"IArray\":{t.length=e.array.length,t.depth=0,t.left=Cs,t.right=Cs;break}case\"ISingleton\":{t.length=1,t.depth=0,t.left=Cs,t.right=Cs;break}case\"ISlice\":{t.length=e.length,t.depth=0,t.left=Cs,t.right=Cs;break}}return t},lh=e=>k(e,bR),Cs=mE({_tag:\"IEmpty\"}),DK=()=>Cs;var LK=e=>mE({_tag:\"ISingleton\",a:e}),hE=e=>lh(e)?e:BK(Ye(e)),pE=(e,t,r)=>{switch(e.backing._tag){case\"IArray\":{SR(e.backing.array,0,t,r,e.length);break}case\"IConcat\":{pE(e.left,t,r),pE(e.right,t,r+e.left.length);break}case\"ISingleton\":{t[r]=e.backing.a;break}case\"ISlice\":{SR(bi(e.backing.chunk),e.backing.offset,t,r,e.length);break}}};var $K=e=>{switch(e.backing._tag){case\"IEmpty\":return ER;case\"IArray\":return e.backing.array;default:{let t=new Array(e.length);return pE(e,t,0),e.backing={_tag:\"IArray\",array:t},e.left=Cs,e.right=Cs,e.depth=0,t}}},bi=$K;var BK=e=>e.length===0?DK():e.length===1?LK(e[0]):mE({_tag:\"IArray\",array:e});var IR=e=>e.length;var gE=Jc,TR=li,wR=f_,CR=l_,AR=p_,kR=Eb,vR=d_;var OR=zm;var _R=Ib;var yE=h_,RR=x_;var SE=g_;var bE=Tb,EE=El,PR=wb;var MR=Cb,Dl=y_;var FR=Il;var NR=vb;var UR=b_;var IE=_b;var su=e=>jK(typeof e==\"string\"?zK.encode(e):e),Ei=e=>{let t=HK(e),r=t.length;if(r%4!==0)return W(new An({kind:\"Decode\",module:\"Base64\",input:t,message:`Length must be a multiple of 4, but is ${r}`}));let n=t.indexOf(\"=\");if(n!==-1&&(n$f(u4(e,t,r));function f4(e){let t=aP(e.name,e.valueEncoded,e.options);if(t!==void 0)throw t;let r=e.name+\"=\"+e.valueEncoded;if(e.options===void 0)return r;let n=e.options;if(n.maxAge!==void 0){let o=vA(Ge(n.maxAge));r+=\"; Max-Age=\"+Math.trunc(o)}if(n.domain!==void 0&&(r+=\"; Domain=\"+n.domain),n.path!==void 0&&(r+=\"; Path=\"+n.path),n.priority!==void 0)switch(n.priority){case\"low\":r+=\"; Priority=Low\";break;case\"medium\":r+=\"; Priority=Medium\";break;case\"high\":r+=\"; Priority=High\";break}if(n.expires!==void 0&&(r+=\"; Expires=\"+n.expires.toUTCString()),n.httpOnly&&(r+=\"; HttpOnly\"),n.secure&&(r+=\"; Secure\"),n.partitioned&&(r+=\"; Partitioned\"),n.sameSite!==void 0)switch(n.sameSite){case\"lax\":r+=\"; SameSite=Lax\";break;case\"strict\":r+=\"; SameSite=Strict\";break;case\"none\":r+=\"; SameSite=None\";break}return r}var cP=e=>{let t={},r=Object.values(e.cookies);for(let n=0;nObject.values(e.cookies).map(f4);var l4=e=>{try{return decodeURIComponent(e)}catch{return e}};var Ai=new WeakMap,fP=e=>{if(Ai.has(e))return Ai.get(e);throw new Error(\"Unable to get redacted value\"+(e.label?` with label: \"${e.label}\"`:\"\"))};var lP=\"~effect/Redacted\",qE=e=>k(e,lP),ki=(e,t)=>{let r=Object.create(m4);return t?.label&&(r.label=t.label),Ai.set(r,e),r},m4={[lP]:{_A:e=>e},label:void 0,...rr,toJSON(){return this.toString()},toString(){return``},[se](){return H(Ai.get(this))},[ae](e){return qE(e)&&J(Ai.get(this),Ai.get(e))}},au=fP;var pP=e=>nr((t,r)=>e(au(t),au(r)));var WE=Symbol.for(\"~effect/http/Headers\"),dP=e=>k(e,WE),JE=Object.defineProperties(Object.create(null),{[WE]:{value:WE},[Na]:{value(e){return x4(this,Fe(e,g4))}},toJSON:{value(){return is(this)}},[ae]:{value(e){return KE(this,e)}},[se]:{value(){return Tf(this)}},toString:{value:dc.toString},[Te]:{value:dc[Te]}});var KE=hA(Ja()),h4=Object.create(JE),mP=e=>{if(e===void 0)return h4;if(Symbol.iterator in e){let r=Object.create(JE);for(let[n,o]of e)r[n.toLowerCase()]=o;return r}let t=Object.create(JE);for(let[r,n]of Object.entries(e))Array.isArray(n)?t[r.toLowerCase()]=n.join(\", \"):n!==void 0&&(t[r.toLowerCase()]=n);return t};var x4=l(2,(e,t)=>{let r={...e},n=o=>{if(typeof o==\"string\"){let s=o.toLowerCase();s in e&&(r[s]=ki(e[s]))}else for(let s in e)s.search(o)!==-1&&(r[s]=ki(e[s]))};if(Array.isArray(t))for(let o=0;o[\"authorization\",\"cookie\",\"set-cookie\",\"x-api-key\",\"AWSAccessKeyId\",\"Signature\",\"sig\",\"X-Goog-Signature\"]});var GE=\"~effect/http/UrlParams\",VE=e=>k(e,GE),S4={...rr,[GE]:GE,[Symbol.iterator](){return this.params[Symbol.iterator]()},toJSON(){return{_id:\"UrlParams\",params:Object.fromEntries(this.params)}},[ae](e){return ZE(this,e)},[se](){return To(this.params.flat())}},Sh=e=>{let t=Object.create(S4);return t.params=e,t},hP=e=>{if(VE(e))return e;let t=xP(e),r=[];for(let n=0;n{let t=typeof e[Symbol.iterator]==\"function\"?Ye(e):Object.entries(e),r=[];for(let[n,o]of t)if(Array.isArray(o))for(let s=0;sb4(e.params,t.params)),b4=SA(cA([Ja(),Ja()]));var gP=l(2,(e,t)=>yy(e.params,([r])=>r===t).pipe(Rt(([,r])=>r)));var E4=e=>{let t={};for(let[r,n]of e.params)if(!Object.hasOwn(t,r))M(t,r,n);else{let o=t[r];typeof o==\"string\"?M(t,r,[o,n]):o.push(n)}return t},yP=E4;var YE=\"~effect/Graph\",QE=e=>k(e,YE),SP=e=>e;var T4=(e,t,r)=>(e===\"directed\"?t.source===r.source&&t.target===r.target:t.source===r.source&&t.target===r.target||t.source===r.target&&t.target===r.source)&&J(t.data,r.data),w4=(e,t)=>e===\"directed\"?H(t):qt(le(H(t.data),C4(t.source,t.target))),C4=(e,t)=>le(0,H(e))+le(0,H(t)),A4=Re(\"Graph\"),k4={[YE]:{_N:e=>e,_E:e=>e},[Symbol.iterator](){return this.nodes[Symbol.iterator]()},[Te](){return this.toJSON()},[ae](e){if(k(e,YE)){let t=SP(e);if(this.nodes.size!==t.nodes.size||this.edges.size!==t.edges.size||this.type!==t.type)return!1;for(let[r,n]of this.nodes)if(!t.nodes.has(r)||!J(n,t.nodes.get(r)))return!1;for(let[r,n]of this.edges){let o=t.edges.get(r);if(o===void 0||!T4(this.type,n,o))return!1}return!0}return!1},[se](){let e=A4;e=e^Re(this.type),e=e^It(this.nodes.size),e=e^It(this.edges.size);for(let[t,r]of this.nodes)e^=le(H(t),H(r));for(let[t,r]of this.edges)e^=le(H(t),w4(this.type,r));return qt(e)},toJSON(){return{_id:\"Graph\",nodeCount:this.nodes.size,edgeCount:this.edges.size,type:this.type}},toString(){return`Graph(${this.type}, ${this.nodes.size}, ${this.edges.size})`},pipe(){return Z(this,arguments)}},v4=(e,t)=>{let r=Object.create(k4);return r.type=e,r.mutable=t,r.transforming=!1,r.nodes=new Map,r.edges=new Map,r.adjacency=new Map,r.reverseAdjacency=new Map,r.nextNodeIndex=0,r.nextEdgeIndex=0,r.acyclic=O(!0),r};var Hl=e=>{let t=SP(e);return{type:e.type,nodes:Array.from(t.nodes,([r,n])=>({index:r,data:n})).sort((r,n)=>r.index-n.index),edges:Array.from(t.edges,([r,n])=>({index:r,source:n.source,target:n.target,data:n.data})).sort((r,n)=>r.index-n.index)}},bh=e=>{let t=v4(e.type,!1);for(let r of e.nodes)t.nodes.set(r.index,r.data),t.adjacency.set(r.index,[]),t.reverseAdjacency.set(r.index,[]);for(let r of e.edges)t.edges.set(r.index,{source:r.source,target:r.target,data:r.data}),t.adjacency.get(r.source).push(r.index),t.reverseAdjacency.get(r.target).push(r.index),e.type===\"undirected\"&&(t.adjacency.get(r.target).push(r.index),t.reverseAdjacency.get(r.source).push(r.index));return t.nextNodeIndex=e.nodes.length===0?0:e.nodes[e.nodes.length-1].index+1,t.nextEdgeIndex=e.edges.length===0?0:e.edges[e.edges.length-1].index+1,t.acyclic=v(),t};function As(e){return e.checks?e.checks[e.checks.length-1].annotations:e.annotations}function Eh(e){return t=>As(t)?.[e]}var Lr=\"~structural\",vi=\"~identifier\",Wl=\"~sentinels\",Ih=\"~constructor\",bP=[\"title\",\"description\",\"default\",\"examples\",\"readOnly\",\"writeOnly\",\"format\",\"contentEncoding\",\"contentMediaType\",\"contentSchema\"],iu=Eh(\"identifier\"),XE=Eh(vi),eI=Eh(\"title\");var Jl=Vr(e=>{let t=As(e)?.identifier;return typeof t==\"string\"?t:e.getExpected(Jl)}),EP=new Set([Wl,Lr,\"representation\",\"arbitraryConstraint\",\"toJsonSchema\",\"toCode\",\"toEquivalence\",\"toFormatter\",\"toCodec\",\"toCodecJson\",\"toCodecStringTree\",\"toCodecIso\",\"toCodecArbitrary\"]);var ge=Symbol();var Zt=vr,$r=Zt(ge),qe=Zt(ge),Kl=e=>e===ge?v():O(e),tI=e=>e._tag===\"None\"?$r:Zt(e.value);var Zl=\"~effect/SchemaIssue/Issue\";function nI(e){return k(e,Zl)&&e[Zl]===Zl}function _4(e){return Object.hasOwn(e,\"input\")}var fn=class{[Zl]=Zl;constructor(t,r){r?.reportInput===!0&&t!==ge&&(this.input=t)}},IP=class extends fn{_tag=\"Filter\";filter;issue;constructor(e,t,r,n){super(r,n),this.filter=e,this.issue=t}},TP=class extends fn{_tag=\"Encoding\";ast;issue;constructor(e,t,r,n){super(r,n),this.ast=e,this.issue=t}},ut=class extends fn{_tag=\"Pointer\";path;issue;constructor(e,t){super(),this.path=e,this.issue=t}},ma=class extends fn{_tag=\"MissingKey\";annotations;constructor(e){super(),this.annotations=e}},oI=class extends fn{_tag=\"UnexpectedKey\";ast;constructor(e,t,r){super(t,r),this.ast=e}},wt=class extends fn{_tag=\"Composite\";ast;issues;constructor(e,t,r,n){super(r,n),this.ast=e,this.issues=t}},ft=class extends fn{_tag=\"InvalidType\";ast;constructor(e,t,r){super(t,r),this.ast=e}},de=class extends fn{_tag=\"InvalidValue\";annotations;constructor(e,t,r){super(t,r),this.annotations=e}};function Br(e,t,r,n,o){return new wt(e,[new ut([t],r)],n,o)}var Yl=class extends fn{_tag=\"Forbidden\";annotations;constructor(e,t,r){super(t,r),this.annotations=e}},Ql=class extends fn{_tag=\"AnyOf\";ast;issues;constructor(e,t,r,n){super(r,n),this.ast=e,this.issues=t}},wP=class extends fn{_tag=\"OneOf\";ast;successes;constructor(e,t,r,n){super(r,n),this.ast=e,this.successes=t}};function rI(e,t,r){if(nI(e))return e;if(typeof e==\"string\")return new de({message:e},t,r);let n=typeof e.issue==\"string\"?new de({message:e.issue},t,r):e.issue;return new ut(e.path,n)}function CP(e,t,r){if(e!==void 0)return typeof e==\"boolean\"?e?void 0:new de(void 0,t,r):rI(e,t,r)}function AP(e,t,r,n){return Array.isArray(t)?tt(t)?t.length===1?rI(t[0],r,n):new wt(e,wn(t,o=>rI(o,r,n)),r,n):void 0:CP(t,r,n)}var kP=e=>{let t=uu(e);if(t!==void 0)return t;switch(e._tag){case\"InvalidType\":return da(Jl(e.ast),e);case\"InvalidValue\":{let r=sI(e);if(r!==void 0)return da(r,e);let n=Th(e);return n===void 0?\"Expected a valid value\":`Invalid data ${n}`}case\"MissingKey\":return\"Missing key\";case\"UnexpectedKey\":{let r=Th(e);return r===void 0?\"Expected no excess property\":`Unexpected key with value ${r}`}case\"Forbidden\":return\"Forbidden operation\";case\"OneOf\":{let r=Th(e);return r===void 0?\"Expected exactly one member to match\":`Expected exactly one member to match the input ${r}`}}},vP=e=>uu(e.issue)??uu(e);function OP(e){return t=>({issues:cu(t,[],e?.leafHook??kP,e?.checkHook??vP)})}function Th(e){return _4(e)?F(e.input):void 0}function sI(e){let t=e.annotations?.expected;return typeof t==\"string\"?t:void 0}function da(e,t){let r=Th(t);return r===void 0?`Expected ${e}`:`Expected ${e}, got ${r}`}function cu(e,t,r,n){switch(e._tag){case\"Filter\":{let o=n(e);if(o!==void 0)return[{path:t,message:o}];if(e.issue._tag!==\"InvalidValue\")return cu(e.issue,t,r,n);let s=sI(e.issue);return[{path:t,message:s===void 0?da(aI(e.filter),e):da(s,e.issue)}]}case\"Encoding\":return cu(e.issue,t,r,n);case\"Pointer\":return cu(e.issue,[...t,...e.path],r,n);case\"Composite\":return e.issues.flatMap(o=>cu(o,t,r,n));case\"AnyOf\":return e.issues.length===0?[{path:t,message:uu(e)??da(Jl(e.ast),e)}]:e.issues.flatMap(o=>cu(o,t,r,n));default:return[{path:t,message:r(e)}]}}function aI(e){let t=e.annotations?.expected;if(typeof t==\"string\")return t;switch(e._tag){case\"Filter\":return\"\";case\"FilterGroup\":return e.checks.map(r=>aI(r)).join(\" & \")}}function R4(){return e=>Vl(e,\"\")}var _P=R4();function Vl(e,t){let r;switch(e._tag){case\"Filter\":{let n=vP(e);if(n!==void 0)r=n;else{if(e.issue._tag!==\"InvalidValue\")return Vl(e.issue,t);let o=sI(e.issue);r=o===void 0?da(aI(e.filter),e):da(o,e.issue)}break}case\"Encoding\":return Vl(e.issue,t);case\"Pointer\":return Vl(e.issue,t+pd(e.path));case\"Composite\":case\"AnyOf\":{if(e._tag===\"Composite\"||e.issues.length>0)return e.issues.map(n=>Vl(n,t)).join(`\n`);r=uu(e)??da(Jl(e.ast),e);break}default:r=kP(e);break}return t?`${r}\n at ${t}`:r}function uu(e){if(e._tag===\"Pointer\")return;if(e._tag===\"Encoding\")return uu(e.issue);let r=(e._tag===\"Filter\"?e.filter.annotations:\"annotations\"in e?e.annotations:e.ast.annotations)?.[e._tag===\"MissingKey\"?\"messageMissingKey\":e._tag===\"UnexpectedKey\"?\"messageUnexpectedKey\":\"message\"];if(typeof r==\"string\")return r}function Xl(e){let t;for(let r of e.reasons){if(!na(r)||!nI(r.error))return;t??=r.error}return t}function ln(e,t){let r=Xl(e);if(r===void 0)throw new Error(t,{cause:e});return r}var PP=e=>su(e).replace(/=/g,\"\").replace(/\\+/g,\"-\").replace(/\\//g,\"_\"),iI=e=>{let t=e.replace(/[\\n\\r]/g,\"\"),r=t.length;if(r%4===1)return W(new An({module:\"Base64Url\",kind:\"Decode\",input:t,message:`Length should be a multiple of 4, but is ${r}`}));if(!/^[-_A-Z0-9]*?={0,2}$/i.test(t))return W(new An({module:\"Base64Url\",kind:\"Decode\",input:t,message:\"Invalid input\"}));let n=r%4===2?`${t}==`:r%4===3?`${t}=`:t;return n=n.replace(/-/g,\"+\").replace(/_/g,\"/\"),Ei(n)},MP=e=>Tr(iI(e),t=>P4.decode(t)),P4=new TextDecoder;var FP=(e,t,r)=>{switch(e._tag){case\"Passthrough\":return g(t);case\"Transform\":return g(Rt(t,e.transform));case\"TransformOptional\":return g(e.transform(t));case\"TransformEffect\":return fe(t)?aa:Dr(e.transform(t.value,r),O);case\"TransformOptionalEffect\":return e.transform(t,r)}};var ep=e=>Object.assign(Object.create(Lg),e),wh=(e,t)=>cI((r,n)=>st(FP(e,r,n),o=>FP(t,o,n))),Ch=l(2,(e,t)=>{if(e._tag===\"Passthrough\")return t;if(t._tag===\"Passthrough\")return e;switch(e._tag){case\"Transform\":switch(t._tag){case\"Transform\":return oe(r=>t.transform(e.transform(r)));case\"TransformOptional\":return Oi(r=>t.transform(Rt(r,e.transform)));case\"TransformEffect\":return gt((r,n)=>t.transform(e.transform(r),n));case\"TransformOptionalEffect\":return wh(e,t)}case\"TransformOptional\":switch(t._tag){case\"Transform\":return Oi(r=>Rt(e.transform(r),t.transform));case\"TransformOptional\":return Oi(r=>t.transform(e.transform(r)));case\"TransformEffect\":case\"TransformOptionalEffect\":return wh(e,t)}case\"TransformEffect\":switch(t._tag){case\"Transform\":return gt((r,n)=>Dr(e.transform(r,n),t.transform));case\"TransformOptional\":case\"TransformOptionalEffect\":return wh(e,t);case\"TransformEffect\":return gt((r,n)=>st(e.transform(r,n),o=>t.transform(o,n)))}case\"TransformOptionalEffect\":return wh(e,t)}});function F4(e){return cI((t,r)=>P(e(t,r)))}function Ah(e){return F4((t,r)=>{let n={message:e(t)};return be(t)?new Yl(n,t.value,r):new Yl(n)})}var NP=Ah(()=>\"Encoding is not supported\"),N4=ep({_tag:\"Passthrough\"});function jr(){return N4}function oe(e){return ep({_tag:\"Transform\",transform:e})}function gt(e){return ep({_tag:\"TransformEffect\",transform:e})}function Oi(e){return ep({_tag:\"TransformOptional\",transform:e})}function cI(e){return ep({_tag:\"TransformOptionalEffect\",transform:e})}function uI(){return Oi(()=>v())}function fI(e){return cI(t=>{let r=Va(t,Io);return be(r)?g(r):Dr(e,O)})}function _i(){return oe(globalThis.String)}function kh(){return oe(globalThis.Number)}function UP(){return oe(globalThis.BigInt)}function lI(){return oe(e=>new globalThis.Date(e))}function DP(){return oe(C_)}function LP(e){return gt((t,r)=>Qc({try:()=>JSON.parse(t,e?.reviver),catch:()=>new de({expected:\"a valid JSON string\"},t,r)}))}function $P(e){return gt((t,r)=>Qc({try:()=>{let n=JSON.stringify(t,e?.replacer,e?.space);if(n===void 0)throw new TypeError(\"Value cannot be represented as JSON\");return n},catch:()=>new de({expected:\"a JSON-serializable value\"},t,r)}))}function BP(e){let t=e?.separator??\",\",r=e?.keyValueSeparator??\"=\";return oe(n=>n.split(t).reduce((o,s)=>{if(r===\"\"){let[u,f]=s.split(\"\");return u&&f&&M(o,u,f),o}let a=s.indexOf(r);if(a<=0)return o;let i=s.slice(0,a),c=s.slice(a+r.length);return c&&M(o,i,c),o},{}))}function jP(e){let t=e?.separator??\",\",r=e?.keyValueSeparator??\"=\";return oe(n=>Object.entries(n).map(([o,s])=>`${o}${r}${s}`).join(t))}function zP(e){let t=e?.separator??\",\";return oe(r=>r===\"\"?[]:r.split(t))}function pI(){return oe(su)}function vh(){return oe(PP)}function Oh(){return oe(zA)}function qP(){return gt((e,t)=>Ul(Pl(Ei(e)),()=>new de({expected:\"a valid Base64 string\"},e,t)))}function HP(){return gt((e,t)=>wr(LR(e),{onFailure:()=>P(new de({expected:\"a valid Base64 string\"},e,t)),onSuccess:g}))}function WP(){return gt((e,t)=>wr(iI(e),{onFailure:()=>P(new de({expected:\"a valid Base64Url string\"},e,t)),onSuccess:g}))}function JP(){return gt((e,t)=>wr(MP(e),{onFailure:()=>P(new de({expected:\"a valid Base64Url string\"},e,t)),onSuccess:g}))}function KP(){return gt((e,t)=>wr(Oy(e),{onFailure:()=>P(new de({expected:\"a valid hexadecimal string\"},e,t)),onSuccess:g}))}function GP(){return gt((e,t)=>wr(qA(e),{onFailure:()=>P(new de({expected:\"a valid hexadecimal string\"},e,t)),onSuccess:g}))}function VP(){return oe(encodeURIComponent)}function ZP(){return gt((e,t)=>{try{return g(globalThis.decodeURIComponent(e))}catch{return P(new de({expected:\"a valid URI component\"},e,t))}})}function YP(){return oe(e=>tM(Array.from(e.entries())))}var U4=rM(e=>typeof e==\"string\"||typeof Blob<\"u\"&&e instanceof Blob);function QP(){return oe(e=>{let t=new FormData;return typeof e==\"object\"&&e!==null&&U4(e).forEach(([n,o])=>{t.append(n,o)}),t})}function XP(){return oe(e=>tM(Array.from(e.entries())))}var D4=rM(br);function eM(){return oe(e=>typeof e==\"object\"&&e!==null?new URLSearchParams(D4(e)):new URLSearchParams)}function L4(e){if(e===\"\")return[\"\"];let t=e.replace(/\\[(.*?)\\]/g,\".$1\"),r=t.split(\".\"),n=t.startsWith(\".\")?1:0;return r.slice(n).map(o=>Cc(o)?globalThis.Number(o):o)}function tM(e){let t={},r=new WeakSet,n=new WeakSet;function o(s,a,i){let c=Object.hasOwn(s,a)?s[a]:void 0;if(r.has(c)&&Array.isArray(c)===i)return c;let u=i?[]:{};return r.add(u),M(s,a,u),u}return e.forEach(([s,a])=>{let i=L4(s),c=t;i.forEach((u,f)=>{let p=f===i.length-1;if(Array.isArray(c)&&u===\"\")if(p)c.push(a);else{let d=i[f+1],m=typeof d==\"number\"||d===\"\",x=c.length;c=o(c,x,m)}else if(p){let d=Object.hasOwn(c,u);if(d&&Array.isArray(c[u])&&(r.has(c[u])||n.has(c[u])))c[u].push(a);else if(d){let m=[c[u],a];n.add(m),M(c,u,m)}else M(c,u,a)}else{let d=i[f+1];c=o(c,u,typeof d==\"number\"||d===\"\")}})}),t}function rM(e){return t=>{let r=[];function n(o,s){if(e(s))r.push([o,s]);else if(Array.isArray(s))s.every(e)?s.forEach(i=>{r.push([o,i])}):s.forEach((i,c)=>{n(`${o}[${c}]`,i)});else if(typeof s==\"object\"&&s!==null)for(let[a,i]of Object.entries(s))n(`${o}[${a}]`,i)}for(let[o,s]of Object.entries(t))n(o,s);return r}}var _h=class extends $s{_tag=\"Middleware\";decode;encode;constructor(e,t){super(),this.decode=e,this.encode=t}flip(){return new _h(this.encode,this.decode)}},tp=\"~effect/SchemaTransformation/Transformation\",ve=class extends $s{[tp]=tp;_tag=\"Transformation\";decode;encode;constructor(e,t){super(),this.decode=e,this.encode=t}flip(){return new ve(this.encode,this.decode)}};function $4(e){return k(e,tp)&&e[tp]===tp}var Rh=e=>$4(e)?e:new ve(e.decode,e.encode);function Yt(e){return new ve(gt(e.decode),gt(e.encode))}function Pe(e){return new ve(oe(e.decode),oe(e.encode))}function Ph(e){return new ve(Oi(e.decode),Oi(e.encode))}function nM(){return new ve(DP(),jr())}function oM(e){return new ve(BP(e),jP(e))}var B4=new ve(jr(),jr());function lu(){return B4}var pu=new ve(kh(),_i()),Mh=new ve(UP(),_i()),dI=new ve(lI(),oe(Yg)),sM=new ve(lI(),oe(e=>e.getTime()));var mI=Yt({decode:(e,t)=>Wt(hR(e),{onNone:()=>P(new de({expected:\"a valid ByteSize string\"},e,t)),onSome:g}),encode:e=>g(`${e} ${e===BigInt(1)?\"byte\":\"bytes\"}`)}),aM=Yt({decode:(e,t)=>Wt(lE(e),{onNone:()=>P(new de({expected:\"a non-negative bigint byte count\"},e,t)),onSome:g}),encode:e=>g(uh(e))}),iM=Yt({decode:(e,t)=>Wt(lE(e),{onNone:()=>P(new de({expected:\"a non-negative safe-integer byte count\"},e,t)),onSome:g}),encode:(e,t)=>Wt(yR(e),{onNone:()=>P(new de({expected:\"a ByteSize representable as a safe integer\"},e,t)),onSome:g})}),j4=e=>fr(e)&&typeof e.message==\"string\",cM=e=>{let r=Object.hasOwn(e,\"cause\")?new Error(e.message,{cause:fM(e.cause)}):new Error(e.message);return typeof e.name==\"string\"&&e.name!==\"Error\"&&(r.name=e.name),typeof e.stack==\"string\"&&(r.stack=e.stack),r},z4=e=>{try{let t=Ua(e);return t===void 0?F(e):JSON.parse(t)}catch{return F(e)}},q4=(e,t,r)=>{let n={name:e.name,message:typeof e.message==\"string\"?e.message:\"\"};return t?.includeStack&&typeof e.stack==\"string\"&&(n.stack=e.stack),!t?.excludeCause&&e.cause!==void 0&&(n.cause=r(e.cause)),n},uM=e=>{let t=new WeakSet,r=n=>{if(cC(n)){if(t.has(n))return\"[Circular]\";t.add(n);let o=q4(n,e,r);return t.delete(n),o}return z4(n)};return r},fM=e=>j4(e)?cM(e):e,lM=e=>Pe({decode:cM,encode:t=>uM(e)(t)}),pM=e=>Pe({decode:fM,encode:uM(e)});function dM(){return Pe({decode:cy,encode:Rd})}function mM(){return Pe({decode:iy,encode:qs})}function hM(e){return Pe({decode:Nf,encode:e?.onNoneEncoding===null?Rd:qs})}function xM(){return Ph({decode:O,encode:Ga})}function gM(){return Ph({decode:e=>e.pipe(Va(Io),O),encode:Ga})}var hI=Yt({decode:(e,t)=>URL.canParse(e)?g(new URL(e)):P(new de({expected:\"a valid URL string\"},e,t)),encode:e=>g(e.href)});var xI=new ve(qP(),pI()),yM=new ve(HP(),pI()),SM=new ve(JP(),vh()),bM=new ve(GP(),Oh()),EM=new ve(ZP(),VP());function IM(e){return new ve(LP(e??{}),$P(e))}var TM=new ve(YP(),QP()),wM=new ve(XP(),eM());function xa(e){return t=>t._tag===e}var gu=xa(\"Declaration\");var H4=xa(\"Never\");var mu=xa(\"Literal\"),yI=xa(\"UniqueSymbol\");var AI=xa(\"Arrays\"),W4=xa(\"Objects\"),MM=xa(\"Union\"),FM=xa(\"Suspend\"),at=class{to;transformation;constructor(e,t){this.to=e,this.transformation=t}},Go={},Ko=class{isOptional;isMutable;constructorDefault;annotations;constructor(e,t,r=void 0,n=void 0){this.isOptional=e,this.isMutable=t,this.constructorDefault=r,this.annotations=n}},CM=\"~effect/Schema\",lt=class{[CM]=CM;annotations;checks;encoding;context;constructor(t=void 0,r=void 0,n=void 0,o=void 0){this.annotations=t,this.checks=r,this.encoding=n,this.context=o}toString(){return`<${this._tag}>`}},yu=class extends lt{_tag=\"Declaration\";typeParameters;run;encodingChecks;encodingRun;constructor(e,t,r,n,o,s,a,i){super(r,n,o,s),this.typeParameters=e,this.run=t,this.encodingChecks=a,this.encodingRun=i}getParser(){let e;return(t,r)=>t===ge?$r:(e??=this.run(this.typeParameters))(t,this,r)}_rebuild(e,t,r,n,o){let s=Pi(this.typeParameters,e);return s===this.typeParameters&&t===this.checks&&r===this.encodingChecks&&n===this.run&&o===this.encodingRun?this:new yu(s,n,this.annotations,t,void 0,this.context,r,o)}recur(e){return this._rebuild(e,this.checks,this.encodingChecks,this.run,this.encodingRun)}flip(e){return this._rebuild(e,this.encodingChecks,this.checks,this.encodingRun??this.run,this.run)}getExpected(){let e=this.annotations?.expected;return typeof e==\"string\"?e:\"\"}},J4=class extends lt{_tag=\"Null\";getParser(){return qh(this,null)}getExpected(){return\"null\"}},kI=new J4;var K4=class extends lt{_tag=\"Undefined\";getParser(){return qh(this,void 0)}toCodecJson(){return We(this,[NM])}getExpected(){return\"undefined\"}},NM=new at(kI,new ve(oe(()=>{}),oe(()=>null))),vI=new K4;var G4=class extends lt{_tag=\"Void\";getParser(){let e=Zt(void 0);return t=>t===ge?$r:e}toCodecJson(){return We(this,[NM])}getExpected(){return\"void\"}},UM=new G4;var V4=class extends lt{_tag=\"Never\";getParser(){return ro(this,iC)}getExpected(){return\"never\"}},OI=new V4,Z4=class extends lt{_tag=\"Any\";getParser(){return ro(this,zg)}getExpected(){return\"any\"}},DM=new Z4,Y4=class extends lt{_tag=\"Unknown\";getParser(){return ro(this,zg)}getExpected(){return\"unknown\"}},eo=new Y4,Q4=class extends lt{_tag=\"ObjectKeyword\";getParser(){return ro(this,If)}getExpected(){return\"object | array | function\"}},LM=new Q4,$M=class extends lt{_tag=\"Enum\";enums;constructor(e,t,r,n,o){super(t,r,n,o);for(let[,s]of e)if(typeof s==\"number\"&&!globalThis.Number.isFinite(s))throw new Error(`A numeric enum value must be finite, got ${F(s)}`);this.enums=e}getParser(){let e=new Set(this.enums.map(([,t])=>t));return ro(this,t=>e.has(t))}toCodecStringTree(){if(this.enums.some(([e,t])=>typeof t==\"number\")){let e=Object.fromEntries(this.enums.map(([t,r])=>[globalThis.String(r),r]));return We(this,[new at(new zr(Object.keys(e).map(t=>new dn(t))),new ve(oe(t=>e[t]),_i()))])}return this}getExpected(){return this.enums.map(([e,t])=>JSON.stringify(t)).join(\" | \")}};function gI(e){return typeof e==\"number\"?`parts[${e}]`:e}function SI(e,t,r){if(r.has(e))return!0;if(e.encoding)throw new Error(`TemplateLiteral parts cannot have an encoding at ${gI(t)}`);let n;switch(e._tag){case\"String\":case\"Number\":case\"BigInt\":n=!0;break;case\"Literal\":n=!e.checks;break;case\"TemplateLiteral\":{let o=gI(t);n=!e.checks&&e.parts.every((s,a)=>SI(s,`${o}.parts[${a}]`,r));break}case\"Union\":{let o=gI(t);n=!e.checks&&e.types.every((s,a)=>SI(s,`${o}.types[${a}]`,r));break}default:return!1}return n&&r.add(e),n}var _I=class extends lt{_tag=\"TemplateLiteral\";parts;encodedParts;literals;suffixLengths;constructor(e,t,r,n,o){super(t,r,n,o);let s=[],a=[],i=new Set;for(let u=0;u=0;u--)c[u]=c[u+1]+(a[u]?.length??0);this.parts=e,this.encodedParts=s,this.literals=a,this.suffixLengths=c}getParser(e){let t=new Pn(!1,this.parts.map(WI),[]),r=e(ga(pn,t,jM(this)));return(n,o)=>{if(n===ge)return $r;let s=r(n,o);return s._tag===\"Success\"?qe:Vt(s,{onSuccess:()=>n,onFailure:a=>new wt(this,[a],n,o)})}}getExpected(){return\"string\"}matchPart(e,t){return dF(this,e,t)===void 0?void 0:e}};function BM(e){let t=Vr(o=>{if(o._tag!==\"Union\")return o;let s=Pi(o.types,t);return(o.options?.mode??\"anyOf\")===\"anyOf\"&&s===o.types?o:new zr(s,o.options===void 0?void 0:{...o.options,mode:\"anyOf\"},o.annotations,o.checks,void 0,o.context)}),r=new _I(e.map(o=>t(hn(o)))),n=new Pn(!1,e.map(WI),[]);return ga(r,n,jM(r))}function jM(e){return new ve(gt((t,r)=>{let n=dF(e,t,r);return n?g(n):P(new de({expected:\"a string matching template literal parts\"},t,r))}),oe(t=>t.join(\"\")))}var zM=class extends lt{_tag=\"UniqueSymbol\";symbol;constructor(e,t,r,n,o){super(t,r,n,o),this.symbol=e}getParser(){return qh(this,this.symbol)}toCodecStringTree(){let e=globalThis.Symbol.keyFor(this.symbol);return We(this,[new at(e===void 0?OI:new dn(globalThis.String(this.symbol)),xF.transformation)])}getExpected(){return globalThis.String(this.symbol)}},dn=class extends lt{_tag=\"Literal\";literal;constructor(e,t,r,n,o){if(super(t,r,n,o),typeof e==\"number\"&&!globalThis.Number.isFinite(e))throw new Error(`A numeric literal must be finite, got ${F(e)}`);this.literal=e}getParser(){return qh(this,this.literal)}matchPart(e,t){return e===globalThis.String(this.literal)?this.literal:void 0}toCodecJson(){return typeof this.literal==\"bigint\"?AM(this):this}toCodecStringTree(){return typeof this.literal==\"string\"?this:AM(this)}getExpected(){return typeof this.literal==\"string\"?JSON.stringify(this.literal):globalThis.String(this.literal)}};function AM(e){let t=globalThis.String(e.literal);return We(e,[new at(new dn(t),new ve(oe(()=>e.literal),oe(()=>t)))])}var X4=class extends lt{_tag=\"String\";getParser(){return ro(this,br)}matchPart(e,t){let r=this.checks;return r&&!t.disableChecks&&no(r,e,void 0,this,t)?void 0:e}getExpected(){return\"string\"}},pn=new X4,e6=class extends lt{_tag=\"Number\";getParser(){return ro(this,Ef)}matchKey(e,t){return this._match(y6,e,t)}matchPart(e,t){return this._match(TI,e,t)}_match(e,t,r){if(!e.test(t))return;let n=globalThis.Number(t);return r.disableChecks||!this.checks?n:no(this.checks,n,void 0,this,r)?void 0:n}toCodecJson(){return this.checks&&(bI(this.checks,\"effect/schema/isFinite\")||bI(this.checks,\"effect/schema/isInt\"))?this:We(this,[l6])}toCodecStringTree(){return this.toCodecJson()===this?We(this,[S6]):We(this,[b6])}getExpected(){return\"number\"}};function bI(e,t){return e.some(r=>r.annotations?.representation?.id===t||r._tag===\"FilterGroup\"&&bI(r.checks,t))}var RI=new e6,t6=class extends lt{_tag=\"Boolean\";getParser(){return ro(this,ad)}getExpected(){return\"boolean\"}},qM=new t6,r6=class extends lt{_tag=\"Symbol\";getParser(){return ro(this,jg)}matchKey(e,t){return t.disableChecks||!this.checks?e:no(this.checks,e,void 0,this,t)?void 0:e}toCodecStringTree(){return We(this,[xF])}getExpected(){return\"symbol\"}},HM=new r6,n6=class extends lt{_tag=\"BigInt\";getParser(){return ro(this,sC)}matchPart(e,t){if(!wI.test(e))return;let r=globalThis.BigInt(e);return t.disableChecks||!this.checks?r:no(this.checks,r,void 0,this,t)?void 0:r}toCodecStringTree(){return We(this,[I6])}getExpected(){return\"bigint\"}},WM=new n6,Pn=class extends lt{_tag=\"Arrays\";isMutable;elements;rest;encodingChecks;constructor(e,t,r,n,o,s,a,i){super(n,o,s,a),this.isMutable=e,this.elements=t,this.rest=r,this.encodingChecks=i;let c=!1;for(let u=0;u1)throw new Error(\"A required element cannot follow an optional element. ts(1257)\");for(let u=1;u=f?o[p-f+1]:o[0]}let c=f=>{let{input:p,len:d,options:m}=f;if(r.rest.length===0&&d>s)for(let x=s;x{let d=f.length,m={ast:r,getParser:i,input:f,len:d,tailThreshold:Math.max(s,d-a),output:new globalThis.Array(d),issues:void 0,options:p},x=r.rest.length===0?s:Math.max(d,s+a),y=p.concurrency===void 0?1:pl(p.concurrency),S=y===1?s6(m,f,0,x):a6(m,f,{concurrency:y,end:x});if(!S)return c(m);if(Ne(S))return st(S,()=>c(m));let w=!0;return $(()=>w?(w=!1,I(S,()=>c(m))):u(f,p))};return(f,p)=>{if(f===ge)return $r;try{return Array.isArray(f)?(n||(n=r.elements.map(d=>({ast:d,parser:t(d)})),o=r.rest.map(d=>({ast:d,parser:t(d)}))),u(f,p)):P(new ft(r,f,p))}catch(d){return an(d)}}}_rebuild(e,t,r){let n=Pi(this.elements,e),o=Pi(this.rest,e);return n===this.elements&&o===this.rest&&t===this.checks&&r===this.encodingChecks?this:new Pn(this.isMutable,n,o,this.annotations,t,void 0,this.context,r)}recur(e){return this._rebuild(e,this.checks,this.encodingChecks)}flip(e){return this._rebuild(e,this.encodingChecks,this.checks)}getExpected(){return\"array\"}};function o6(e,t,r,n){if(r._tag===\"Failure\")return Nh(e,e.ast,n,r);let o=r===qe?t:r[ue];if(o!==ge)e.output[n]=o;else{let s=e.getParser(e.tailThreshold,n);if(Qt(s.ast))return;let a=new ut([n],new ma(s.ast.context?.annotations));if(e.options.errors===\"all\")e.issues?e.issues.push(a):e.issues=[a];else return bs(new wt(e.ast,[a],e.input,e.options))}}var JM={onItem(e,t,r){let n=r{if(n.cause.reasons.length===0)return n;let o=Xl(n.cause);if(o===void 0)return sn(Ss(n.cause,a=>new wt(t,[new ut([r],a)],e.input,e.options)));let s=new ut([r],o);if(e.options.errors===\"all\")e.issues?e.issues.push(s):e.issues=[s];else return bs(new wt(t,[s],e.input,e.options))},Dh=\"[+-]?\\\\d*\\\\.?\\\\d+(?:[Ee][+-]?\\\\d+)?\";function ha(e,t,r=Go){let n,o;function s(a){switch(a._tag){case\"String\":case\"TemplateLiteral\":return(n??=Object.keys(e)).filter(i=>a.matchPart(i,r)!==void 0);case\"Number\":return(n??=Object.keys(e)).filter(i=>a.matchKey(i,r)!==void 0);case\"Symbol\":return(o??=Object.getOwnPropertySymbols(e)).filter(i=>Object.prototype.propertyIsEnumerable.call(e,i)&&a.matchKey(i,r)!==void 0);case\"Union\":return[...new Set(a.types.flatMap(s))];default:return[]}}return s(HI(hn(t)))}var PI=class{name;type;constructor(e,t){this.name=e,this.type=t}};function KM(e){switch(e._tag){case\"String\":case\"Number\":case\"Symbol\":case\"TemplateLiteral\":return!0;case\"Union\":return e.types.every(KM);default:return!1}}function GM(e){let t=bu(e);switch(t._tag){case\"String\":case\"Number\":case\"Symbol\":case\"TemplateLiteral\":return!0;case\"Union\":return t.types.every(GM);default:return!1}}function i6(e){return KM(e)&&GM(e)}var MI=class{parameter;type;constructor(e,t){if(!i6(e))throw new Error(`Invalid index signature parameter ${e._tag}`);if(this.parameter=e,this.type=t,Qt(t)&&!pF(t))throw new Error(\"Cannot use `Schema.optionalKey` with index signatures, use `Schema.optional` instead.\")}},np=class extends lt{_tag=\"Objects\";propertySignatures;indexSignatures;encodingChecks;constructor(e,t,r,n,o,s,a){super(r,n,o,s),this.propertySignatures=e,this.indexSignatures=t,this.encodingChecks=a}getParser(e,t=e){let r=this,n=r.propertySignatures.length,o=r.indexSignatures.length;if(!n&&!o)return ro(r,cd);let s,a,i=()=>(s||(s=r.propertySignatures.map(p=>({parser:t(p.type),name:p.name,type:p.type})),a=o?r.indexSignatures.map(p=>({is:p,parserKey:e(HI(p.parameter)),parserValue:t(p.type)})):void 0),s),c=()=>{let p=new Set(r.propertySignatures.map(S=>typeof S.name==\"number\"?globalThis.String(S.name):S.name)),d=(S,w,E,U,_)=>{if(_._tag===\"Failure\")return Nh(S,r,w,_)??xt;let h=_===qe?U:_[ue];if(E!==ge&&h!==ge){if(n&&(p.has(w)||p.has(typeof E==\"number\"?globalThis.String(E):E)))return xt;M(S.out,E,h)}return xt},m=(S,w,E,U)=>{if(!U){let T=E.parserKey(w,S.options);if(!Ne(T))return I(vn(T),C=>m(S,w,E,C));U=T}if(U._tag===\"Failure\")return Nh(S,r,w,U)??xt;let _=U===qe?w:U[ue],h=S.input[w],b=E.parserValue(h,S.options);return Ne(b)?d(S,w,_,h,b):I(vn(b),T=>d(S,w,_,h,T))},x=(S,w,E)=>{let U=S.input[w],_=E.parserValue(U,S.options);return Ne(_)?d(S,w,w,U,_):I(vn(_),h=>d(S,w,w,U,h))},y=o?vm()({onItem:(S,[w,E])=>E.is.parameter===pn?x(S,w,E):m(S,w,E),step:(S,w,E)=>E._tag===\"Failure\"?E:void 0}):void 0;return oE(function*(S,w){if(S===ge)return ge;if(!(typeof S==\"object\"&&S!==null&&!Array.isArray(S)))return yield*P(new ft(r,S,w));i();let E=S,U={},_={ast:r,input:E,out:U,issues:void 0,options:w},h=w.errors===\"all\",b=w.onExcessProperty===\"error\",T=w.concurrency===void 0?1:pl(w.concurrency),C=o&&b?r.indexSignatures.map(L=>ha(E,L.parameter,w)):void 0;if(b){let L=C?new Set(p):p;if(C)for(let ye of C)for(let _e of ye)L.add(_e);let ee=Reflect.ownKeys(E);for(let ye=0;ye{let x=s[d];return I(vn(m),y=>{let S=EI(p,x,y);if(S)return S;let w=()=>Zt(p.out),E=kM(p,s.slice(d+1));return E?st(E,w):w()})};return(p,d)=>{if(p===ge)return $r;if(d.errors===\"all\"||d.onExcessProperty!==void 0||d.concurrency!==void 0&&pl(d.concurrency)!==1)return(u??=c())(p,d);if(!(typeof p==\"object\"&&p!==null&&!Array.isArray(p)))return P(new ft(r,p,d));let m=i(),x=p,y={},S={ast:r,input:x,out:y,issues:void 0,options:d};try{for(let w=0;w{let i=e(a.type);return i===a.type?a:new PI(a.name,i)}),s=Pi(this.indexSignatures,a=>{let i=t(a.parameter),c=e(a.type);return i===a.parameter&&c===a.type?a:new MI(i,c)});return o===this.propertySignatures&&s===this.indexSignatures&&r===this.checks&&n===this.encodingChecks?this:new np(o,s,this.annotations,r,void 0,this.context,n)}flip(e){return this._rebuild(e,e,this.encodingChecks,this.checks)}recur(e,t=e){return this._rebuild(e,t,this.checks,this.encodingChecks)}getExpected(){return this.propertySignatures.length===0&&this.indexSignatures.length===0?\"object | array\":\"object\"}};function EI(e,t,r){if(r._tag===\"Failure\")return Nh(e,e.ast,t.name,r);if(r===qe)return;let n=r[ue];if(n!==ge){M(e.out,t.name,n);return}if(delete e.out[t.name],!Qt(t.type)){let o=new ut([t.name],new ma(t.type.context?.annotations));if(e.options.errors===\"all\"){e.issues?e.issues.push(o):e.issues=[o];return}else return bs(new wt(e.ast,[o],e.input,e.options))}}var VM={onItem(e,t){if(!du(e.input,t.name))return t.parser(ge,e.options);let r=e.input[t.name];return M(e.out,t.name,r),t.parser(r,e.options)},step:EI},kM=km()(VM),c6=vm()(VM);function FI(e,t){return e?t?[...e,...t]:e:t}function Lh(e,t,r){return new np(Reflect.ownKeys(e).map(n=>new PI(n,e[n].ast)),[],r,t)}function op(e){return e.ast}function NI(e,t=void 0){return new Pn(!1,e.map(r=>r.ast),[],void 0,t)}function $h(e,t,r){return new zr(e.map(op),t,void 0,r)}function ZM(e,t){if(e.encoding||t.some(s=>s.encoding))throw new Error(\"StructWithRest does not support encodings\");let r=e.propertySignatures,n=e.indexSignatures,o=e.checks;for(let s of t){for(let a of s.propertySignatures)if(r.some(i=>i.name===a.name))throw new Error(`Duplicate identifier: ${JSON.stringify(a.name)}. ts(2300)`);r=r.concat(s.propertySignatures),n=n.concat(s.indexSignatures),o=FI(o,s.checks)}return new np(r,n,void 0,o)}function YM(e,t){if(e.encoding)throw new Error(\"TupleWithRest does not support encodings\");return new Pn(e.isMutable,e.elements,t,void 0,e.checks)}function QM(e){if(e.encoding)throw new Error(\"mutable does not support encodings\");return new Pn(!0,e.elements,e.rest,e.annotations,e.checks,void 0,e.context,e.encodingChecks)}var UI=Pa(e=>{for(;;){if(FM(e))return eo;let t=e.encoding;if(!t)return e.recur?.(UI,R)??e;if(t.some(r=>r.transformation._tag===\"Middleware\"&&r.transformation.decode!==R))return eo;e=t[t.length-1].to}});function XM(e){switch(e._tag){case\"Null\":return[\"null\"];case\"Undefined\":return[\"undefined\"];case\"String\":case\"TemplateLiteral\":return[\"string\"];case\"Number\":return[\"number\"];case\"Boolean\":return[\"boolean\"];case\"Symbol\":case\"UniqueSymbol\":return[\"symbol\"];case\"BigInt\":return[\"bigint\"];case\"Arrays\":return[\"array\"];case\"ObjectKeyword\":return[\"object\",\"array\",\"function\"];case\"Objects\":return e.propertySignatures.length||e.indexSignatures.length?[\"object\"]:[\"string\",\"number\",\"boolean\",\"symbol\",\"bigint\",\"object\",\"array\",\"function\"];case\"Enum\":return Array.from(new Set(e.enums.map(([,t])=>typeof t)));case\"Literal\":return[typeof e.literal];case\"Union\":return Array.from(new Set(e.types.flatMap(XM)));default:return[\"null\",\"undefined\",\"string\",\"number\",\"boolean\",\"symbol\",\"bigint\",\"object\",\"array\",\"function\"]}}function hu(e){switch(e._tag){default:return[];case\"Declaration\":{let t=e.annotations?.[Wl];return Array.isArray(t)?t:[]}case\"Objects\":return e.propertySignatures.flatMap(t=>{let r=t.type;if(!Qt(r)){if(mu(r))return[{key:t.name,literal:r.literal}];if(yI(r))return[{key:t.name,literal:r.symbol}]}return[]});case\"Arrays\":return e.elements.flatMap((t,r)=>{if(!Qt(t)){if(mu(t))return[{key:r,literal:t.literal}];if(yI(t))return[{key:r,literal:t.symbol}]}return[]});case\"Union\":{if(e.types.length===0)return[];let t=e.types.map(r=>hu(UI(r)));return t[0].filter(r=>t.every(n=>n.some(o=>o.key===r.key&&o.literal===r.literal)))}case\"Suspend\":return hu(e.thunk())}}var vM=new WeakMap,Fh=Object.freeze([]),OM=e=>e===null?\"null\":Array.isArray(e)?\"array\":typeof e,du=(e,t)=>t===\"__proto__\"?Object.hasOwn(e,t):t in e;function sp(e){let t=vM.get(e);if(t)return t;let r,n=0,o,s,a=!0,i=[];for(let f=0;fc[f]??=Object.freeze(o?.[f]??Fh);if(a&&s)s.forEach(Object.freeze),t=f=>s.get(f)??Fh;else if(r?.size===1&&!o){let[f,[p]]=r.entries().next().value,d=new Map;for(let[x,y]of p)d.set(x,Object.freeze(Array.from(y)));let m=Object.freeze(e.map((x,y)=>y));t=(x,y)=>{if(If(x)){let S=du(x,f)?x[f]:void 0;if(S!==void 0)return d.get(S)??Fh;if(y)return m}return Fh}}else if(r){let f;for(let p of r)(!f||p[1][0].size>f[1][0].size)&&p[1][1].size===n&&(f=p);t=(p,d)=>{let m=OM(p);if(!If(p))return u(m);let x=new Set(o?.[m]),y;if(f){let[S,[w]]=f,E=du(p,S),U=E?p[S]:void 0;if(E&&(!d||U!==void 0)){let _=w.get(U);if(!_)return u(m);for(let h of _)x.add(h);y=S}}if(y===void 0)for(let[S,[w,E]]of r){let U=du(p,S),_=U?p[S]:void 0;if(U&&(!d||_!==void 0)){let h=w.get(_);if(h)for(let b of h)x.add(b)}else if(d)for(let h of E)x.add(h)}for(let[S,[w,E]]of r){if(S===y)continue;let U=du(p,S),_=U?p[S]:void 0;if(U&&(!d||_!==void 0)){let h=w.get(_);for(let b of x)E.has(b)&&!h?.has(b)&&x.delete(b)}}return Array.from(x).sort((S,w)=>S-w)}}else t=f=>{let p=u(OM(f));return s?p.filter(d=>i[d]===void 0||i[d]===f):p};return vM.set(e,t),t}var zr=class extends lt{_tag=\"Union\";types;options;encodingChecks;constructor(e,t,r,n,o,s,a){super(r,n,o,s),this.types=e,this.options=t,this.encodingChecks=a}getParser(e,t){let r=this,n=t!==void 0,o=[],s=i=>o[i]??=e(r.types[i]),a;return(i,c)=>{if(i===ge)return $r;let u=(a??=sp(r.types))(i,n);if(u.length===0)return P(new Ql(r,[],i,c));if(u.length===1){let f=s(u[0])(i,c);return f._tag===\"Success\"?f:Ne(f)?eF(r,f.cause,i,c):u6(r,f,i,c)}return tF(r,s,u,i,c)}}_rebuild(e,t,r){let n=Pi(this.types,e);return n===this.types&&t===this.checks&&r===this.encodingChecks?this:new zr(n,this.options,this.annotations,t,void 0,this.context,r)}recur(e){return this._rebuild(e,this.checks,this.encodingChecks)}flip(e){return this._rebuild(e,this.encodingChecks,this.checks)}matchPart(e,t){for(let r of this.types){let n=r.matchPart(e,t);if(n!==void 0)return n}}getExpected(e){let t=this.annotations?.expected;if(typeof t==\"string\")return t;if(this.types.length===0)return\"never\";let r=this.types.map(n=>{let o=hn(n);switch(o._tag){case\"Arrays\":{let s=o.elements.filter(mu);if(s.length>0)return`${RM(o.isMutable)}[ ${s.map(a=>e(a)+PM(a.context?.isOptional)).join(\", \")}, ... ]`;break}case\"Objects\":{let s=o.propertySignatures.filter(a=>mu(a.type));if(s.length>0)return`{ ${s.map(a=>`${RM(a.type.context?.isMutable)}${wo(a.name)}${PM(a.type.context?.isOptional)}: ${e(a.type)}`).join(\", \")}, ... }`;break}}return e(o)});return Array.from(new Set(r)).join(\" | \")}};function eF(e,t,r,n){let o=Xl(t);return o?bs(new Ql(e,[o],r,n)):sn(t)}function u6(e,t,r,n){return rt(t,o=>eF(e,o,r,n))}function tF(e,t,r,n,o){let s={ast:e,parser:t,input:n,out:void 0,successes:e.options?.mode===\"oneOf\"?[]:void 0,issues:void 0,options:o},a=f6(s,r);if(!a)return s.out?s.out:P(new Ql(e,s.issues??[],n,o));if(Ne(a))return _M(a,s);let i=!0;return $(()=>i?(i=!1,_M(a,s)):tF(e,t,r,n,o))}function _M(e,t){return st(e,r=>t.out===qe?g(t.input):t.out?t.out:P(new Ql(t.ast,t.issues??[],t.input,t.options)))}var f6=km()({onItem(e,t){return e.parser(t)(e.input,e.options)},step(e,t,r){if(r._tag===\"Failure\"){let n=Xl(r.cause);if(n===void 0)return r;e.issues?e.issues.push(n):e.issues=[n]}else{if(e.out&&e.successes)return e.successes.push(e.ast.types[t]),bs(new wP(e.ast,e.successes,e.input,e.options));if(e.out=r,e.successes)e.successes.push(e.ast.types[t]);else return xt}}}),rF=new zr([new dn(\"Infinity\"),new dn(\"-Infinity\"),new dn(\"NaN\")]);function RM(e){return e?\"\":\"readonly \"}function PM(e){return e?\"?\":\"\"}var DI=class extends lt{_tag=\"Suspend\";thunk;constructor(e,t,r,n,o){if(r)throw new Error(\"Cannot add checks to Suspend\");super(t,void 0,n,o);let s;this.thunk=()=>s??=e()}getParser(e){let t;return(r,n)=>(t??=e(this.thunk()))(r,n)}recur(e){return new DI(()=>e(this.thunk()),this.annotations,void 0,void 0,this.context)}getExpected(e){return e(this.thunk())}},Uh=class extends $s{_tag=\"Filter\";run;annotations;aborted;constructor(e,t=void 0,r=!1){super(),this.run=e,this.annotations=t,this.aborted=r}annotate(e){return new Uh(this.run,{...this.annotations,...e},this.aborted)}abort(){return new Uh(this.run,this.annotations,!0)}and(e,t){return new Ri([this,e],t)}},Ri=class extends $s{_tag=\"FilterGroup\";checks;annotations;constructor(e,t=void 0){super(),this.checks=e,this.annotations=t}annotate(e){return new Ri(this.checks,{...this.annotations,...e})}and(e,t){return new Ri([this,e],t)}};function Bh(e,t,r=!1){return new Uh((n,o,s)=>AP(o,e(n,o,s),n,s),t,r)}function jh(e,t){return new Uh((r,n,o)=>e(r)?void 0:new de(void 0,r,o),t,!0)}function LI(e){return Bh(t=>globalThis.Number.isFinite(t),{expected:\"a finite number\",representation:{id:\"effect/schema/isFinite\",payload:null},toJsonSchema:()=>({type:\"number\"}),toCode:()=>({runtime:\"Schema.isFinite()\"}),arbitraryConstraint:{number:\"finite\"},...e})}var $I=Vo(RI,[LI()]),l6=new at(new zr([$I,rF]),new ve(kh(),oe(e=>globalThis.Number.isFinite(e)?e:globalThis.String(e))));function ap(e,t){let r=new globalThis.RegExp(e),n={source:r.source,flags:r.flags};return Bh(o=>(r.lastIndex=0,r.test(o)),{expected:`a string matching the RegExp ${n.source}`,representation:{id:\"effect/schema/isPattern\",payload:n},toJsonSchema:()=>[{},!0],arbitraryConstraint:{patterns:[n]},...t})}var II=new WeakMap;function ip(e,t){let r=Object.assign(Object.create(Object.getPrototypeOf(e)),e,t);return Reflect.ownKeys(t).every(n=>n===\"context\"||n===\"encoding\")&&II.set(r,Su(e)),r}function Su(e){let t=II.get(e);if(t!==void 0)return t;if(e.encoding===void 0)return e;let r=Object.assign(Object.create(Object.getPrototypeOf(e)),e,{encoding:void 0});return II.set(e,r),r}function We(e,t){return e.encoding===t?e:ip(e,{encoding:t})}function Mi(e,t){if(e.context===t)return e;let r=Su(e);return r.context===t&&r.encoding===e.encoding?r:ip(e,{context:t})}function bu(e){return e.encoding?bu(e.encoding[e.encoding.length-1].to):e}function to(e,t){if(e.checks){let r=e.checks[e.checks.length-1];return nF(e,$d(e.checks.slice(0,-1),r.annotate(t)))}return ip(e,{annotations:{...e.annotations,...t}})}function nF(e,t){if(e._tag===\"Suspend\"&&t)throw new Error(\"Cannot add checks to Suspend\");return e.checks===t?e:ip(e,{checks:t})}function Vo(e,t){return nF(e,FI(e.checks,t))}function Eu(e,t){let r=t(e.to);return r===e.to?e:new at(r,e.transformation)}function BI(e,t){let r=e,n=r[r.length-1],o=Eu(n,t);return o===n?e:$d(e.slice(0,e.length-1),o)}function jI(e){return t=>t.encoding?We(t,BI(t.encoding,e)):t}function zh(e,t){return jI(r=>Mi(r,t))(e)}function oF(e){function t(r){return r.encoding?We(r,BI(r.encoding,t)):e(r)}return Vr(t)}function Fi(e,t){function r(n){if(n.encoding){let o=n.encoding[n.encoding.length-1];return t?.stopAt?.(o)?n:We(n,BI(n.encoding,r))}return e(n)}return Pa(r)}function sF(e,t){return zI(e,t,yt(e))}function aF(e,t){return zI(hn(e),t,e)}function zI(e,t,r){let n=new at(e,t);return We(r,r.encoding?[...r.encoding,n]:[n])}function Pi(e,t){let r;for(let n=0;n{let t=e.context?e.context.isOptional===!1?new Ko(!0,e.context.isMutable,e.context.constructorDefault,e.context.annotations):e.context:new Ko(!0,!1);return p6(Mi(e,t))}),p6=jI(Ni),iF=Vr(e=>Ni(new zr([e,vI]))),qI=Pa(e=>{let t=e.context?e.context.isMutable===!1?new Ko(e.context.isOptional,!0,e.context.constructorDefault,e.context.annotations):e.context:new Ko(!1,!0);return d6(Mi(e,t))}),d6=jI(qI);function cF(e,t){let r=e.context?new Ko(e.context.isOptional,e.context.isMutable,t,e.context.annotations):new Ko(!1,!1,t);return Mi(e,r)}function ga(e,t,r){return zI(e,r,t)}function m6(e){let t=[],r=[];function n(o){switch(o._tag){case\"Literal\":id(o.literal)&&!t.includes(o.literal)&&t.push(o.literal);return;case\"UniqueSymbol\":t.includes(o.symbol)||t.push(o.symbol);return;case\"Never\":return;case\"Union\":for(let s=0;snew PI(o,t)),n.map(o=>new MI(o,t)))}function Qt(e){return e.context?.isOptional??!1}function fF(e){return e.context?.isMutable??!1}function lF(e){return e.annotations?.[Lr]===!0||e._tag===\"FilterGroup\"&&e.checks.every(lF)}function h6(e){function t(n){return lF(n)?[n]:n._tag===\"FilterGroup\"?n.checks.flatMap(t):[]}let r=e.flatMap(t);return or(r)?r:void 0}function x6(e){let t=!0;function r(n){return t=t&&!n.encoding&&!FM(n),t&&\"recur\"in n&&n.recur(r),n}return\"recur\"in e&&e.recur(r),t}var yt=Pa(e=>{let t=Su(e);if(t!==e){let n=yt(t);return n===t&&e.encoding===void 0?e:Mi(n,e.context)}let r=\"recur\"in e?e.recur(yt):e;if(\"encodingChecks\"in r&&r.encodingChecks){let n=x6(e)?r.encodingChecks:AI(r)||W4(r)||gu(r)&&r.typeParameters.length>0?h6(r.encodingChecks):void 0;return ip(r,{encodingChecks:void 0,checks:FI(r.checks,n)})}return r}),hn=Pa(e=>yt(mn(e)));function g6(e,t){let r=t,n=r.length,o=r[n-1],s=[new at(mn(We(e,void 0)),r[0].transformation.flip())];for(let i=1;i{if(e.encoding)return g6(e,e.encoding);let t=Su(e);if(t!==e){let r=mn(t);return r===t?e:Mi(r,e.context)}return\"flip\"in e?e.flip(mn):\"recur\"in e?e.recur(mn):e});function pF(e){switch(e._tag){case\"Undefined\":return!0;case\"Union\":return e.types.some(pF);default:return!1}}function qh(e,t){let r=t===0?qe:Zt(t);return(n,o)=>n===ge?$r:n===t?r:P(new ft(e,n,o))}function ro(e,t){return(r,n)=>r===ge?$r:t(r)?qe:P(new ft(e,r,n))}function dF(e,t,r){let n=e.encodedParts,o=e.literals,s=t.length;for(let u=0;us)return;let a=new Array(n.length),i;function c(u,f){if(u===n.length)return f===s;if(i?.has(u*(s+1)+f))return!1;let p=n[u];if(u===n.length-1){let d=t.slice(f);if(p.matchPart(d,r)!==void 0)return a[u]=d,!0}else if(p._tag===\"Literal\"){let d=o[u];if(t.startsWith(d,f)&&c(u+1,f+d.length))return a[u]=d,!0}else{let d=s-e.suffixLengths[u+1],m=o[u+1],x=m===void 0?d:t.lastIndexOf(m,d);for(;x>=f;){let y=t.slice(f,x);if(p.matchPart(y,r)!==void 0&&c(u+1,x))return a[u]=y,!0;if(x===0)break;x=m===void 0?x-1:t.lastIndexOf(m,x-1)}}return i??=new Set,i.add(u*(s+1)+f),!1}return c(0,0)?a:void 0}var HI=Fi(e=>{switch(e._tag){default:return e;case\"Number\":return e.toCodecStringTree();case\"Union\":return e.recur(HI)}}),Hh=Fi(e=>{switch(e._tag){default:return e;case\"Symbol\":case\"UniqueSymbol\":return e.toCodecStringTree();case\"Union\":return e.recur(Hh)}}),WI=Fi(e=>{switch(e._tag){default:return e;case\"Number\":case\"Literal\":case\"BigInt\":return e.toCodecStringTree();case\"Union\":return e.recur(WI)}}),mF=\"[\\\\s\\\\S]*?\",TI=new globalThis.RegExp(`^${Dh}$`),y6=new globalThis.RegExp(`^(?:${Dh}|Infinity|-Infinity|NaN)$`);function JI(e){return ap(TI,{expected:\"a string representing a finite number\",representation:{id:\"effect/schema/isStringFinite\",payload:null},toJsonSchema:()=>({pattern:TI.source}),...e})}var hF=Vo(pn,[JI()]),S6=new at(hF,pu),b6=new at(new zr([hF,rF]),pu),E6=\"-?\\\\d+\",wI=new globalThis.RegExp(`^${E6}$`);function KI(e){return ap(wI,{expected:\"a string representing a bigint\",representation:{id:\"effect/schema/isStringBigInt\",payload:null},toJsonSchema:()=>({pattern:wI.source}),...e})}var GI=Vo(pn,[KI({expected:\"a string representing a bigint\"})]),I6=new at(GI,Mh),T6=\"Symbol\\\\(([\\\\s\\\\S]*)\\\\)\",CI=new globalThis.RegExp(`^${T6}$`),w6=Vo(pn,[VI()]),xF=new at(w6,new ve(oe(e=>globalThis.Symbol.for(CI.exec(e)[1])),gt((e,t)=>globalThis.Symbol.keyFor(e)!==void 0?g(globalThis.String(e)):P(new Yl({message:\"cannot serialize to string, Symbol is not registered\"},e,t)))));function VI(e){return ap(CI,{expected:\"a string representing a symbol\",representation:{id:\"effect/schema/isStringSymbol\",payload:null},toJsonSchema:()=>({pattern:CI.source}),...e})}function no(e,t,r,n,o){for(let s=0;s0;){let o=n[n.length-1],s=o.keys;if(typeof s==\"number\"){if(o.index(e,t,r)=>Iu(e)?qe:P(new ft(t,e,r)),{representation:{id:\"effect/schema/Json\",payload:null},expected:\"JSON value\",toCodecJson:()=>{},toCodecStringTree:()=>YI}),bF=to(xu,{representation:{id:\"effect/schema/MutableJson\",payload:null}}),ZI=new at(xu,lu()),EF=new at(new zr([new Pn(!1,[],[xu]),new np([],[new MI(pn,xu)])]),lu());function k6(e){return SF(e,A6)}var v6=new yu([],()=>(e,t,r)=>k6(e)?qe:P(new ft(t,e,r)),{expected:\"StringTree\",toCodecStringTree:()=>{}}),YI=new at(v6,lu());var Wh=(e,t,r)=>e===qe?r(t):st(e,r);function TF(e){if(e._tag===\"Middleware\")return(r,n,o)=>{let s=r===qe?e.decode(Zt(Kl(n)),o):e.decode(Dr(r,Kl),o);return IF(s)};let t=e.decode;switch(t._tag){case\"Passthrough\":return(r,n)=>r===qe?Zt(n):r;case\"Transform\":{let r=n=>n===ge?$r:Zt(t.transform(n));return(n,o)=>Wh(n,o,r)}case\"TransformOptional\":{let r=n=>tI(t.transform(Kl(n)));return(n,o)=>Wh(n,o,r)}case\"TransformEffect\":return(r,n,o)=>Wh(r,n,s=>s===ge?$r:t.transform(s,o));case\"TransformOptionalEffect\":return(r,n,o)=>Wh(r,n,s=>IF(t.transform(Kl(s),o)))}}var IF=e=>st(e,tI),wF=(e,t,r,n)=>rt(n,o=>Is(()=>Ss(o,s=>new TP(e,s,t,r))));function O6(e,t){let r=TF(e.link.transformation),n;return(o,s)=>{if(o===ge)return $r;if(e.isConstructed(o))return qe;let a=(n??=t(e.link.to))(o,s);return r(a,o,s)}}function _6(e,t){let r=e.context.constructorDefault;return(n,o)=>{if(n!==ge&&n!==void 0)return t(n,o);let s=r;if(Ne(s)&&s._tag===\"Success\"){let a=t(s[ue],o);return a===qe?s:a}return st(wF(e,n,o,s),a=>{let i=t(a,o);return i===qe?Zt(a):i})}}function QI(e,t){let r=t(e);return e.context?.constructorDefault===void 0?r:_6(e,r)}function up(e,t,r,n,o){if(e._tag===\"Declaration\")for(let x of e.typeParameters)t(x);let s=r?yF(e):void 0,a=s?O6(s,t):n??e.getParser(t,r),i=e.checks,c=e.encoding,u=c?.map(x=>TF(x.transformation)),f=e.encodingChecks;if(!c&&!i&&!f)return a;let p,d=(x,y)=>{let S=a(x,y);if(f&&!y.disableChecks)if(Ne(S)){if(S._tag===\"Success\"){let w=S===qe?x:S[ue];if(x!==ge&&w!==ge){let E=no(f,x,void 0,e,y);E&&(S=P(new wt(e,E,x,y)))}}}else S=I(S,w=>{if(x!==ge&&w!==ge){let E=no(f,x,void 0,e,y);if(E)return P(new wt(e,E,x,y))}return g(w)});if(i&&!y.disableChecks)if(Ne(S)){if(S._tag===\"Success\"){let w=S===qe?x:S[ue];if(w===ge)return S;let E=no(i,w,void 0,e,y);E&&(S=P(new wt(e,E,w,y)))}}else S=I(S,w=>{if(w!==ge){let E=no(i,w,void 0,e,y);if(E)return P(new wt(e,E,w,y))}return g(w)});return S},m=o===void 0?d:o(d);return c?(x,y)=>{let S=p??=c.map(U=>t(U.to)),w=x,E=S[S.length-1](x,y);for(let U=c.length-1;U>=0;U--)if(E=u[U](E,w,y),U!==0){let _=S[U-1];E._tag===\"Success\"?(w=E[ue],E=_(w,y)):E=st(E,h=>{let b=_(h,y);return b===qe?Zt(h):b})}if(E._tag===\"Success\"){let U=E[ue],_=m(U,y);return _===qe?E:_}return E=wF(e,x,y,E),st(E,U=>{let _=m(U,y);return _===qe?Zt(U):_})}:m}var Gh=Symbol(),CF=new WeakMap,AF,Tu=!1;var P6=e=>Tu?Kh(gn,e,\"parser\"):gn(e).parser,kF=e=>Tu?Kh(gn,e,\"makeEffect\"):gn(e).makeEffect,M6=e=>QI(e,kF),Jh=class{ast;constructor(t){this.ast=t}get decodeEffect(){return this.cachedDecodeEffect??=up(this.ast,P6)}get parser(){return this.decodeEffect}get makeEffect(){return this.cachedMakeEffect??=up(this.ast,kF,M6)}},XI=class extends Jh{compiled;resolve;constructor(t,r,n){super(t),this.compiled=r,this.resolve=n}save(t,r){return Object.defineProperty(this,t,{value:r}),r}operation(t){let r=this.compiled;return typeof r==\"function\"?r(this.ast,this.resolve,t):r?.[t]}get is(){return this.save(\"is\",this.operation(\"is\"))}get decode(){return this.save(\"decode\",this.operation(\"decode\"))}get make(){return this.save(\"make\",this.operation(\"make\"))}get decodeEffect(){return this.save(\"decodeEffect\",this.operation(\"decodeEffect\")??up(this.ast,t=>Kh(this.resolve,t,\"parser\")))}get parser(){let t=this.decode;return t===void 0?this.decodeEffect:this.save(\"parser\",F6(t,()=>this.decodeEffect))}get makeEffect(){let t=this.operation(\"makeEffect\");if(t!==void 0)return this.save(\"makeEffect\",t);let r=n=>Kh(this.resolve,n,\"makeEffect\");return this.save(\"makeEffect\",up(this.ast,r,n=>QI(n,r)))}};function F6(e,t){let r;return(n,o)=>{if(n!==ge)try{let s=e(n,o);if(s!==Gh)return s===n?qe:Zt(s)}catch(s){return an(s)}return(r??=t())(n,o)}}function Kh(e,t,r){let n=e(t);if(n.compiled===void 0||Object.hasOwn(n,r))return n[r];let o;return(s,a)=>(o??=n[r])(s,a)}function gn(e){let t=CF.get(e);if(t!==void 0)return t;let r=AF===void 0?new Jh(e):new XI(e,AF(e,gn),gn);return CF.set(e,r),r}function tT(e){let t=e.ast,r;return(n,o)=>(r??=cT(q6,yt(t)))(n,o?.disableChecks?o?.parseOptions?{...o.parseOptions,disableChecks:!0}:{disableChecks:!0}:o?.parseOptions)}function Zh(e){let t=tT(e);return(r,n)=>{let o=Wo(t(r,n));return Ve(o)?O(o.value):(ln(o.cause,\"Option adapter can only return none for schema issues\"),v())}}function Yh(e){return j6(yt(e.ast))}function rT(e){return Ui(e.ast)}function U6(e){if(!Tu){let o=wu(ex(e));return s=>{let a=o(s,Go);return Ve(a)?!0:(ln(a.cause,\"Type guard adapter can only return false for schema issues\"),!1)}}let t=gn(e),r=t.is;if(r!==void 0)return o=>{try{return r(o,Go)}catch(s){return ln(ci(s),\"Type guard adapter can only return false for schema issues\"),!1}};let n=t.parser;return o=>{let s=Wo(iT(n(o,Go),o));return Ve(s)?!0:(ln(s.cause,\"Type guard adapter can only return false for schema issues\"),!1)}}function Ui(e){let t=yt(e),r=n=>(r=U6(t),r(n));return n=>r(n)}function vF(e){let t=ex(e);return(r,n)=>{let o=Wo(t(r,n));if(!Ve(o))return ln(o.cause,\"Issue adapter can only return schema issues\")}}function OF(e,t){let r=yt(e.ast),n=gn(r).parser(t,Go),o=Wo(iT(n,t));if(Uo(o)){let s=ln(o.cause,\"Assertion adapter can only throw schema issues\");throw new Error(\"Schema validation failed\",{cause:s})}}function nt(e,t){let r=ex(e.ast);return t===void 0?r:(n,o)=>r(n,aT(t,o))}var nT=nt;function _F(e,t){return wu(nt(e,t))}function oT(e,t){return DF(nt(e,t))}var RF=oT;function PF(e,t){return LF(nt(e,t))}function D6(e,t){return Tu?$F(e.ast,t):uT(nt(e,t))}var Qh=D6;function Di(e,t){let r=ex(mn(e.ast));return t===void 0?r:(n,o)=>r(n,aT(t,o))}var MF=Di;function FF(e,t){return wu(Di(e,t))}function sT(e,t){return DF(Di(e,t))}var NF=sT;function UF(e,t){return LF(Di(e,t))}function L6(e,t){return Tu?$F(mn(e.ast),t):uT(Di(e,t))}var Xh=L6,aT=(e,t)=>t?{...e,...t}:e,Vh=e=>e===ge?P(new de):g(e);function ex(e){return cT(z6,e)}function iT(e,t){return e===qe?g(t):Ne(e)?e[ue]===ge?Vh(ge):e:st(e,Vh)}function cT(e,t){let r;return(n,o)=>{let s=(r??=e(t))(n,o??Go);return s===qe?g(n):Ne(s)?s[ue]===ge?Vh(ge):s:st(s,Vh)}}function wu(e){return(t,r)=>Wo(e(t,r))}function DF(e){let t=wu(e);return(r,n)=>{let o=t(r,n);return Ve(o)?O(o.value):(ln(o.cause,\"Option adapter can only return none for schema issues\"),v())}}function LF(e){let t=wu(e);return(r,n)=>{let o=t(r,n);return Ve(o)?B(o.value):W(ln(o.cause,\"Result adapter can only return schema issues\"))}}function $F(e,t){let r;return(n,o)=>(r??=$6(gn(e),t))(n,o)}function $6(e,t){let r=e.decode,n,o=r===void 0?(s,a=Go)=>(n??=eT(e))(s,a):(s,a=Go)=>{if(s===ge)return(n??=eT(e))(s,a);let i;try{i=r(s,a)}catch(c){throw ln(ci(c),\"Sync adapter can only throw schema issues\"),c}return i!==Gh?i:(n??=eT(e))(s,a)};return t===void 0?o:(s,a)=>o(s,aT(t,a))}function eT(e){return uT(cT(()=>e.decodeEffect,e.ast))}function uT(e){let t=wu(e);return(r,n)=>{let o=t(r,n);if(Ve(o))return o.value;let s=ln(o.cause,\"Sync adapter can only throw schema issues\");throw new Error(\"Schema validation failed\",{cause:s})}}function B6(e,t){let r=Wo(e);if(Ve(r))return r.value;let n=ln(r.cause,t);throw new Error(\"Schema validation failed\",{cause:n})}function j6(e){let t,r;return(n,o)=>{t??=gn(e);let s=o?.disableChecks?o.parseOptions?{...o.parseOptions,disableChecks:!0}:{disableChecks:!0}:o?.parseOptions??Go,a=t.make;if(a!==void 0&&n!==ge){let c;try{c=a(n,s)}catch(u){throw ln(ci(u),\"Constructor adapter can only throw schema issues\"),u}if(c!==Gh&&c!==ge)return c}let i=(r??=t.makeEffect)(n,s);return B6(iT(i,n),\"Constructor adapter can only throw schema issues\")}}var z6=e=>gn(e).parser,q6=e=>gn(e).makeEffect;var tx=\"~effect/Schema/Schema\",BF=Symbol(),H6={[tx]:tx,get make(){let e=Yh(this);return Object.defineProperty(this,\"make\",{value:e,enumerable:!0}),e},get makeEffect(){let e=tT(this);return Object.defineProperty(this,\"makeEffect\",{value:e,enumerable:!0}),e},get makeOption(){let e=Zh(this);return Object.defineProperty(this,\"makeOption\",{value:e,enumerable:!0}),e},pipe(){return Z(this,arguments)},annotate(e){return this.rebuild(to(this.ast,e))},annotateKey(e){return this.rebuild(cp(this.ast,e))},check(...e){return this.rebuild(Vo(this.ast,e))},rebuild(e){return oo(e,this[BF])}};function oo(e,t){function r(){}let n=Object.setPrototypeOf(r,H6);return t&&(Object.hasOwn(t,\"name\")||Object.hasOwn(t,\"length\")||Object.hasOwn(t,\"__proto__\"))?Object.defineProperties(n,Object.getOwnPropertyDescriptors({...t})):Object.assign(n,t),n[BF]=t,n.ast=e,n}function fT(e){return e.replace(/~/g,\"~0\").replace(/\\//g,\"~1\")}function fp(e){return e.replace(/~1/g,\"/\").replace(/~0/g,\"~\")}function lT(e){return encodeURI(fT(e)).replace(/#/g,\"%23\")}function pT(e){if(e.length===0||e===\"#\")return\"\";if(!e.startsWith(\"#\"))return;let t=e.slice(1);try{if(encodeURI(t).replace(/%25/g,\"%\").replace(/#/g,\"%23\")!==t)return;let r=decodeURIComponent(t);return r.startsWith(\"/\")&&!/~(?:[^01]|$)/.test(r)?r:void 0}catch{return}}function zF(e){let t=pT(e);return t===void 0?void 0:t.length===0?[]:t.slice(1).split(\"/\").map(fp)}function qF(e){return e.reduce((t,r)=>`${t}/${lT(r)}`,\"#\")}var J6=\"http://json-schema.org/draft-07/schema#\",HF=\"https://json-schema.org/draft/2020-12/schema\";function WF(e,t){return e===t||e===(t.endsWith(\"#\")?t.slice(0,-1):`${t}#`)}function GF(e){return{dialect:\"draft-07\",...Y6(e,iG)}}function K6(e,t){return r(e,!1,!0);function r(n,o,s=!1){if(!fr(n))return n;let a=o||!s&&YF(n.$id),i={};for(let c of Object.keys(n)){let u=n[c],f=u;switch(c){case\"$defs\":case\"properties\":case\"patternProperties\":case\"dependentSchemas\":f=mT(u,p=>r(p,a))??u;break;case\"allOf\":case\"anyOf\":case\"oneOf\":case\"prefixItems\":f=Array.isArray(u)?u.map(p=>r(p,a)):u;break;case\"not\":case\"additionalProperties\":case\"propertyNames\":case\"unevaluatedProperties\":case\"items\":case\"contains\":case\"unevaluatedItems\":case\"if\":case\"then\":case\"else\":case\"contentSchema\":f=r(u,a)}M(i,c,f)}return t(i,a),i}}function VF(e,t){return K6(e,r=>{G6(r,t)})}function G6(e,t){typeof e.$ref==\"string\"&&M(e,\"$ref\",t(e.$ref,\"$ref\")),typeof e.$dynamicRef==\"string\"&&M(e,\"$dynamicRef\",t(e.$dynamicRef,\"$dynamicRef\"))}function mT(e,t){if(!fr(e))return;let r={};for(let n of Object.keys(e))M(r,n,t(e[n],n));return r}function V6(e,t,r){let n=new Map,o=[],s=dT;function a(d,m=[],x=[]){if(m.length===0&&t?.trackIds){let y=fr(d)?f(d):void 0;s=JF(y,dT)??dT}return u(d,m,x,{sourceRoot:[],targetRoot:[],uri:s})}function i(){for(let[d,m,x]of o){let y=m,S=ZF(m,x);if(S!==void 0){let w=zF(S.hash);if(S.hash=\"\",w!==void 0){let E=n.get(KF(S.href,w));E!==void 0&&(y=Z6(m,E))}}M(d,\"$ref\",y)}}let c=r(a);return i(),c;function u(d,m,x,y){if(typeof d==\"boolean\")return p(m,x,y),t?.booleanAdapter?.(d)??d;if(!fr(d))return d;let S=y,w=f(d);if(m.length>0&&t?.trackIds&&YF(w)){let _=JF(w,y.uri);_!==void 0&&(S={parent:y,sourceRoot:m,targetRoot:x,uri:_})}p(m,x,S);let E=S.uri,U={isDocumentRoot:m.length===0,schema(_,h,b=h){return u(_,[...m,h],[...x,b],S)},schemaAt(_,h,b){return u(_,[...m,...h],[...x,...b],S)},schemaArray(_,h,b=h){return Array.isArray(_)?_.map((T,C)=>u(T,[...m,h,String(C)],[...x,b,String(C)],S)):_},schemaMap(_,h,b=h){return fr(_)?mT(_,(T,C)=>u(T,[...m,h,C],[...x,b,C],S)):_},reference(_,h){typeof h==\"string\"?o.push([_,h,E]):M(_,\"$ref\",h)}};return e(d,U)}function f(d){return t?.ignoreRefSiblings===!0&&typeof d.$ref==\"string\"?void 0:d.$id}function p(d,m,x){x.parent!==void 0&&p(d,m,x.parent),n.set(KF(x.uri,d.slice(x.sourceRoot.length)),m.slice(x.targetRoot.length))}}var dT=\"https://effect.invalid/.json-schema/\";function ZF(e,t){return URL.canParse(e,t)?new URL(e,t):void 0}function JF(e,t){if(typeof e!=\"string\")return;let r=ZF(e,t);if(r!==void 0)return r.hash=\"\",r.href}function Z6(e,t){let r=e.indexOf(\"#\");return r===-1&&t.length===0?e:`${r===-1?e:e.slice(0,r)}${qF(t)}`}function KF(e,t){return`${e}\\0${JSON.stringify(t)}`}function YF(e){return typeof e==\"string\"&&e.length>0&&e[0]!==\"#\"}function Y6(e,t,r){return V6(t,{...r,trackIds:!0},n=>({schema:n(e.schema),definitions:mT(e.definitions,(o,s)=>n(o,[\"$defs\",s],[\"definitions\",s]))}))}var Q6=new Set([\"properties\",\"patternProperties\"]),X6=new Set([\"allOf\",\"anyOf\",\"oneOf\"]),eG=new Set([\"not\",\"additionalProperties\",\"propertyNames\",\"contains\",\"if\",\"then\",\"else\",\"contentSchema\"]);function tG(e,t,r,n,o,s=Q6){let a;if(s.has(t))a=n.schemaMap(r,t);else if(X6.has(t))a=n.schemaArray(r,t);else if(o.has(t))a=n.schema(r,t);else return!1;return M(e,t,a),!0}var QF=[\"$anchor\",\"$defs\",\"$dynamicAnchor\",\"$dynamicRef\",\"$vocabulary\",\"contentSchema\",\"dependentRequired\",\"dependentSchemas\",\"maxContains\",\"minContains\",\"prefixItems\",\"unevaluatedItems\",\"unevaluatedProperties\"],Iie=[...QF,\"deprecated\"],Tie=[...QF,\"$comment\",\"$id\",\"$schema\",\"const\",\"contains\",\"contentEncoding\",\"contentMediaType\",\"else\",\"examples\",\"if\",\"patternProperties\",\"propertyNames\",\"then\"],rG=/^[A-Za-z_][-A-Za-z0-9._]*$/,nG=/^[A-Za-z][-A-Za-z0-9._:]*$/;function Li(e,t,r){throw new Error(`Cannot convert JSON Schema keyword \"${e}\" to ${t}: ${r}`)}function oG(e,t,r,n){for(let o of t)Object.hasOwn(e,o)&&Li(o,r,`it is not active in ${n} but would become active in the target`)}var sG=[\"additionalItems\",\"definitions\",\"dependencies\"];function aG(e,t,r,n,o){r.isDocumentRoot?M(e,\"$schema\",WF(t,HF)?n:t):WF(t,HF)||Li(\"$schema\",o,\"an embedded resource cannot declare a different dialect\")}function iG(e,t){oG(e,sG,\"Draft-07\",\"Draft 2020-12\");let r={},n,o,s;for(let a of Object.keys(e)){let i=e[a];if(!tG(r,a,i,t,eG))switch(a){case\"$ref\":n=i;break;case\"$schema\":aG(r,i,t,J6,\"Draft-07\");break;case\"$id\":case\"$anchor\":break;case\"$defs\":M(r,\"definitions\",t.schemaMap(i,a,\"definitions\"));break;case\"prefixItems\":o=i;break;case\"items\":s=i;break;case\"dependentRequired\":case\"dependentSchemas\":case\"minContains\":case\"maxContains\":break;case\"$dynamicRef\":case\"$dynamicAnchor\":case\"$vocabulary\":case\"unevaluatedProperties\":case\"unevaluatedItems\":Li(a,\"Draft-07\",\"the target dialect has no equivalent\");case\"required\":if(Array.isArray(i)&&i.length===0)break;M(r,a,i);break;default:M(r,a,i)}}if(cG(r,o,s,t),Object.hasOwn(e,\"contains\")){let a=e.minContains,i=e.maxContains;(a!==void 0&&a!==1||i!==void 0)&&Li(\"minContains/maxContains\",\"Draft-07\",\"contains cardinality cannot be represented\"),Object.hasOwn(e,\"minContains\")&&M(r,\"minContains\",a)}else Object.hasOwn(e,\"minContains\")&&M(r,\"minContains\",e.minContains),Object.hasOwn(e,\"maxContains\")&&M(r,\"maxContains\",e.maxContains);return fG(e,r,t,\"draft-07\"),pG(e,r,\"$id\",\"Draft-07\"),uG(r,n,t),r}function cG(e,t,r,n){if(t===void 0){r!==void 0&&M(e,\"items\",n.schema(r,\"items\"));return}M(e,\"items\",n.schemaArray(t,\"prefixItems\",\"items\")),r!==void 0&&M(e,\"additionalItems\",n.schema(r,\"items\",\"additionalItems\"))}function uG(e,t,r){if(t!==void 0)if(typeof t==\"string\"&&Object.keys(e).length>0){let n={};r.reference(n,t),lG(e,n)}else r.reference(e,t)}function fG(e,t,r,n){let o=fr(e.dependentRequired)?e.dependentRequired:void 0,s=fr(e.dependentSchemas)?e.dependentSchemas:void 0;if(o===void 0&&s===void 0)return;let a={},i=new Set([...Object.keys(o??{}),...Object.keys(s??{})]);for(let c of i){let u=o?.[c],f=s?.[c],p=n===\"draft-04\"&&Array.isArray(u)&&u.length===0;f===void 0?p||M(a,c,u):u===void 0||p?M(a,c,r.schemaAt(f,[\"dependentSchemas\",c],[\"dependencies\",c])):M(a,c,{allOf:[r.schemaAt(f,[\"dependentSchemas\",c],[\"dependencies\",c,\"allOf\",\"0\"]),{required:u}]})}Object.keys(a).length>0&&M(t,\"dependencies\",a)}function lG(e,t){Array.isArray(e.allOf)?e.allOf.push(t):M(e,\"allOf\",[t])}function pG(e,t,r,n){let o=e.$id,s=e.$anchor;if(s===void 0){o!==void 0&&M(t,r,o);return}(typeof s!=\"string\"||!rG.test(s))&&Li(\"$anchor\",n,\"the anchor is not valid\"),nG.test(s)||Li(\"$anchor\",n,\"the anchor cannot be represented as a plain-name fragment identifier\"),o===void 0?M(t,r,`#${s}`):Li(\"$anchor\",n,\"it cannot be combined with the schema $id\")}function rx(e){return oo(Cu(e.ast),{schema:e})}var Cu=Fi(e=>{let t=mG(e,Cu),r=e.context;return t===e||r===void 0?t:zh(t,hT(r))});function hT(e){return e.constructorDefault===void 0?e:new Ko(e.isOptional,e.isMutable,void 0,e.annotations)}function tN(e){if(e.propertySignatures.some(t=>typeof t.name!=\"string\"))throw new globalThis.Error(\"Objects property names must be strings\",{cause:e})}function rN(e){return t=>{let r=[...t].sort((o,s)=>e(hn(o))-e(hn(s)));return r.some((o,s)=>o!==t[s])?r:t}}var dG=rN(e=>{switch(e._tag){case\"BigInt\":case\"Symbol\":case\"UniqueSymbol\":return 0;default:return 1}});function mG(e,t){switch(e._tag){case\"Declaration\":{let r=e.annotations?.toCodecJson??e.annotations?.toCodec;if(!Zr(r))return We(e,[ZI]);let n=e.typeParameters.map(s=>oo(hn(s))),o=r(n);return o===void 0?e:We(e,[Eu(o,t)])}case\"Unknown\":return We(e,[ZI]);case\"ObjectKeyword\":return We(e,[EF]);case\"Undefined\":case\"Void\":case\"Literal\":case\"Number\":return e.toCodecJson();case\"UniqueSymbol\":case\"Symbol\":case\"BigInt\":return e.toCodecStringTree();case\"Objects\":return tN(e),e.recur(t,Hh);case\"Union\":{let r=dG(e.types);return r!==e.types?new zr(r,e.options,e.annotations,e.checks,e.encoding,e.context,e.encodingChecks).recur(t):e.recur(t)}case\"Arrays\":case\"Suspend\":return e.recur(t)}return e}function nx(e){return oo(nN(yt(e.ast)))}var nN=Vr(e=>{let t=hG(e,nN);return t!==e&&e.context!==void 0?zh(t,hT(e.context)):t});function hG(e,t){switch(e._tag){case\"Declaration\":{let r=e.annotations?.toCodecIso??e.annotations?.toCodec;if(Zr(r)){let n=r(e.typeParameters.map(o=>oo(o)));return We(e,[Eu(n,t)])}return e}case\"Arrays\":case\"Objects\":case\"Union\":case\"Suspend\":return e.recur(t)}return e}function ox(e){return oo(iN(e.ast),{schema:e})}function oN(e){return oo(cN(e.ast))}var xG=rN(e=>{switch(e._tag){case\"Null\":case\"Boolean\":case\"Number\":case\"BigInt\":case\"Symbol\":case\"UniqueSymbol\":return 0;default:return 1}});function gG(e,t,r){switch(e._tag){case\"Declaration\":{let n=e.typeParameters.map(u=>oo(t(hn(u)))),o=e.annotations?.toCodecStringTree;if(Zr(o)){let u=o(n);return u===void 0?e:We(e,[Eu(u,t)])}let s=e.annotations?.toCodecJson,a=Zr(s)?s(n):void 0,i=a===void 0?e.annotations?.toCodec:void 0,c=a??(Zr(i)?i(n):void 0);return c===void 0?r(e):We(e,[Eu(c,t)])}case\"Null\":return We(e,[yG]);case\"Boolean\":return We(e,[SG]);case\"Unknown\":case\"ObjectKeyword\":return We(e,[YI]);case\"Enum\":case\"Number\":case\"Literal\":case\"UniqueSymbol\":case\"Symbol\":case\"BigInt\":return e.toCodecStringTree();case\"Objects\":return tN(e),e.recur(t,Hh);case\"Union\":{let n=xG(e.types);return n!==e.types?new zr(n,e.options,e.annotations,e.checks,e.encoding,e.context,e.encodingChecks).recur(t):e.recur(t)}case\"Arrays\":case\"Suspend\":return e.recur(t)}return e}var yG=new at(new dn(\"null\"),new ve(oe(()=>null),oe(()=>\"null\"))),SG=new at(new zr([new dn(\"true\"),new dn(\"false\")]),new ve(oe(e=>e===\"true\"),_i())),sN=new ve(oe(e=>typeof e==\"string\"?[e]:e),jr()),aN=e=>e.transformation===sN,iN=Fi(e=>{let t=gG(e,iN,r=>{throw new globalThis.Error(\"Missing structural codec for StringTree\",{cause:r})});return t!==e&&e.context!==void 0?zh(t,hT(e.context)):t},{stopAt:aN}),eN=e=>Qt(e)?Ni(eo):eo,cN=Fi(e=>{let t=bG(e);if(AI(t)){let r=ga(new zr([new Pn(t.isMutable,t.elements.map(eN),t.rest.map(eN)),pn],{mode:\"anyOf\"}),t,sN);return Qt(e)?Ni(r):r}return t},{stopAt:aN});function bG(e){return e._tag===\"Declaration\"||e._tag===\"Arrays\"||e._tag===\"Objects\"||e._tag===\"Union\"||e._tag===\"Suspend\"?e.recur(cN):e}var Cie=globalThis.RegExp;var ks=e=>e.replace(/[/\\\\^$*+?.()|[\\]{}]/g,\"\\\\$&\");function ya(e,t){return t.length>0&&(e+=`\n at ${pd(t)}`),new Error(e)}function uN(e){return`#/$defs/${lT(e)}`}var EG=new Set([...EP,vi,...bP]);function pp(e,t){if(e===void 0)return;let r={},n=e.title;typeof n==\"string\"&&(r.title=n);let o=e.description,s=e.expected;typeof o==\"string\"?r.description=o:t?.generateDescriptions===!0&&typeof s==\"string\"&&(r.description=s);let a=e.default;Iu(a)&&(r.default=a);let i=e.examples;Array.isArray(i)&&Iu(i)&&(r.examples=i);let c=e.readOnly;typeof c==\"boolean\"&&(r.readOnly=c);let u=e.writeOnly;typeof u==\"boolean\"&&(r.writeOnly=u);let f=e.format;typeof f==\"string\"&&(r.format=f);let p=e.contentEncoding;typeof p==\"string\"&&(r.contentEncoding=p);let d=e.contentMediaType;typeof d==\"string\"&&(r.contentMediaType=d);let m=e.contentSchema;if(Iu(m)&&(r.contentSchema=m),t?.includeAnnotationKey!==void 0)for(let[x,y]of Object.entries(e))EG.has(x)||!t.includeAnnotationKey(x)||Iu(y)&&M(r,x,y);return Object.keys(r).length===0?void 0:r}function lN(e){let t=e.type===\"number\"||e.type===\"integer\"?e.type:void 0,r=e;if(t!==void 0&&(r={...e},delete r.type),Array.isArray(r.allOf)){let n=[],o=!1;for(let s of r.allOf){let a=lN(s);a.type!==void 0&&(o=!0,(t===void 0||a.type===\"integer\")&&(t=a.type)),Object.keys(a.schema).length>0&&n.push(a.schema)}if(o){let{allOf:s,...a}=r;r=n.length===0?a:{...a,allOf:n}}}return{type:t,schema:r}}function IG(e){if(!Array.isArray(e.anyOf)||e.anyOf.length!==2||e.anyOf[0]?.type!==\"number\")return!1;let t=e.anyOf[1];return t?.type===\"string\"&&Array.isArray(t.enum)&&t.enum.length===3&&t.enum.includes(\"NaN\")&&t.enum.includes(\"Infinity\")&&t.enum.includes(\"-Infinity\")}var pN=\"|type|format|pattern|multipleOf|minimum|maximum|exclusiveMinimum|exclusiveMaximum|minLength|maxLength|minItems|maxItems|uniqueItems|minProperties|maxProperties|propertyNames|\";function sx(e,t){return Object.keys(e).every(r=>t.includes(`|${r}|`))}function fN(e,t){return typeof e.$ref!=\"string\"&&t.every(r=>!Object.hasOwn(e,r))}var yT=\"|title|description|default|examples|readOnly|writeOnly|\",TG=pN+yT;function dp(e,t,r){if(Object.keys(e).length===0)return t;let n=Object.keys(t);if(n.length===0)return e;let o=e.type===\"number\"||e.type===\"integer\"?e.type:void 0,s=IG(e);if(o!==void 0||s){let i=lN(t);if(i.type!==void 0){let c=o===\"integer\"||i.type===\"integer\"?\"integer\":\"number\",u={...e,type:c};s&&delete u.anyOf;let f=Object.keys(i.schema);return f.length===0?u:sx(i.schema,yT)&&fN(u,f)?{...u,...i.schema}:dp(u,i.schema,r)}}if(r&&fN(e,n))return{...e,...t};let a=Array.isArray(t.allOf)&&n.length===1?t.allOf:[t];return Array.isArray(e.allOf)?{...e,allOf:[...e.allOf,...a]}:typeof e.$ref==\"string\"?{allOf:[e,...a]}:{...e,allOf:a}}function gT(e,t=new Set){return typeof e==\"boolean\"?e:t.has(e)?!0:(t.add(e),e.every(r=>gT(r,t)))}function wG(e){return Array.isArray(e)}function CG(e,t,r,n){let o=new Map,s=new Map,a=new WeakMap,i=new Map,c=new Map,u=Object.keys(r);for(let h of u)x(h,[\"references\",h]);let f=wn(e,(h,b)=>E(h,t[b],[]));for(let[h,b]of i.values())if(!gT(h))for(let T of b)Array.isArray(T.oneOf)&&(T.anyOf=T.oneOf,delete T.oneOf);for(let h of u){let b=r[h],T=S(b);if(T===void 0)continue;let C=o.get(h)[0],L=c.get(T),ee=L?.find(ye=>J(o.get(ye)[0],C));ee===void 0?L===void 0?c.set(T,[h]):L.push(h):s.set(h,ee)}let p=wn(f,y),d={};for(let h of u)s.has(h)||M(d,h,y(o.get(h)[0]));return{dialect:\"draft-2020-12\",schemas:p,definitions:d};function m(h){return Array.isArray(h.oneOf)&&i.get(h.oneOf)?.[1].push(h),h}function x(h,b){if(!Object.hasOwn(r,h))throw ya(`Invalid reference ${h}`,[...b,\"$ref\"]);let T=o.get(h);if(T!==void 0)return T;let C=[{},[]];return o.set(h,C),C[0]=E(r[h],[\"references\",h],C[1]),C}function y(h){return VF(h,b=>{let T=pT(b);if(T===void 0){if(/^#(?:\\/|%2f)/i.test(b))throw new Error(`Invalid JSON Pointer URI fragment ${JSON.stringify(b)}`);return b}if(!T.startsWith(\"/$defs/\"))return b;let C=T.indexOf(\"/\",7),L=s.get(fp(T.slice(7,C<0?void 0:C)));return L===void 0?b:b.replace(/^#(?:\\/|%2f).*?(?:\\/|%2f).*?(?=\\/|%2f|$)/i,uN(L))})}function S(h){if(h._tag===\"Reference\")return;let b=h.checks.length===0?h.annotations:h.checks[h.checks.length-1].annotations;return typeof b?.identifier!=\"string\"&&typeof b?.[vi]==\"string\"?b[vi]:void 0}function w(h,b,T,C){let L=h.annotations,ee=L?.toJsonSchema;if(ee!==void 0){let Ze=h.representation?.schemas?.map((gf,X$)=>E(gf,[...T,\"representation\",\"schemas\",X$],C))??[],Ls=ee({type:b,schemas:Ze}),et=wG(Ls);et&&C.push(!1);let zt=et?Ls[0]:Ls,ur=pp(L,n),ss=ur===void 0?zt:{...zt,...ur},_a=ur===void 0?pN:TG;return[ss,h._tag===\"Filter\"&&sx(ss,_a)&&(ur===void 0||sx(ur,yT))?!0:void 0]}if(h._tag===\"Filter\"){C.push(!1);return}let ye=h.checks.flatMap((Ze,Ls)=>{let et=w(Ze,b,[...T,\"checks\",Ls],C);return et===void 0?[]:[et[0]]});if(ye.length===0)return;let _e=pp(L,n),Et={allOf:ye};return[_e===void 0?Et:{...Et,..._e}]}function E(h,b,T){if(h._tag===\"Reference\")return T.push(x(h.$ref,b)[1]),{$ref:uN(h.$ref)};let C=a.get(h);if(C!==void 0)return T.push(C[1]),C[0];let L=[];T.push(L);let ee=U(h,b,L),ye=pp(h.annotations,n);ye!==void 0&&(ee={...ee,...ye});for(let _e=0;_e{if(typeof ee==\"number\"&&!globalThis.Number.isFinite(ee))throw ya(`Invalid numeric enum value ${globalThis.String(ee)}`,[...b,\"enums\",ye,1]);return{type:typeof ee,enum:[ee],title:L}});return C.length===0?{not:{}}:{anyOf:C}}case\"TemplateLiteral\":return{type:\"string\",pattern:`^${h.parts.map(ax).join(\"\")}$`};case\"Arrays\":{if(h.rest.length>1)throw ya(\"Invalid schema representation document\",[...b,\"rest\"]);let C={type:\"array\"},L=h.elements.length,ee=h.elements.map((ye,_e)=>{ye.isOptional&&L--;let Et=E(ye.type,[...b,\"elements\",_e,\"type\"],T),Ze=pp(ye.annotations,n);return Ze===void 0?Et:m(dp(Et,Ze))});if(ee.length>0?(C.prefixItems=ee,C.maxItems=h.elements.length,L>0&&(C.minItems=L)):C.items=!1,h.rest.length===1){delete C.maxItems;let ye=E(h.rest[0],[...b,\"rest\",0],T);Object.keys(ye).length>0?C.items=ye:delete C.items}return C}case\"Objects\":{if(h.propertySignatures.length===0&&h.indexSignatures.length===0)return{not:{type:\"null\"}};let C={type:\"object\"},L={},ee=[];for(let et=0;et0&&(C.properties=L),ee.length>0&&(C.required=ee);let ye={},_e=[],Et=[],Ze=!1;for(let et=0;et0;if(Ls&&(C.patternProperties=ye),Ze&&n?.onExcessProperty===\"error\"){let et=[];h.propertySignatures.length>0&&et.push({enum:Object.keys(L)});for(let zt=0;zt0&&(C.allOf=_e.map(zt=>({type:\"object\",additionalProperties:zt})))}else _e.length===0?C.additionalProperties=Ze||n?.onExcessProperty!==\"error\":_e.length===1&&h.propertySignatures.length===0&&!Ls?C.additionalProperties=_e[0]:_e.length>0&&(C.allOf=_e.map(et=>({type:\"object\",additionalProperties:et})));return typeof C.additionalProperties==\"object\"&&C.additionalProperties!==null&&Object.keys(C.additionalProperties).length===0&&delete C.additionalProperties,C}case\"Union\":{let C=[],L=h.types.map((ye,_e)=>E(ye,[...b,\"types\",_e],C));if(T.push(C),L.length===0)return{not:{}};let ee=h.options?.mode??\"anyOf\";if(ee===\"anyOf\"&&L.length>1){let ye=kG(L);if(ye!==void 0)return ye}return ee===\"oneOf\"&&i.set(L,[C,[]]),{[ee]:L}}}}function _(h,b,T){switch(h._tag){case\"Reference\":{if(!Object.hasOwn(r,h.$ref))throw ya(`Invalid reference ${h.$ref}`,[...b,\"$ref\"]);if(x(h.$ref,b),T.has(h.$ref))return;let C=new Set(T).add(h.$ref);return _(r[h.$ref],[\"references\",h.$ref],C)}case\"String\":{if(h.checks.length===0)return[];if(h.checks.length!==1||h.checks[0]._tag!==\"Filter\"||h.checks[0].annotations?.toJsonSchema===void 0)return;let C=[],L=E(h,b,C);return gT(C)&&L.type===\"string\"&&typeof L.pattern==\"string\"&&sx(L,\"|type|pattern|title|description|default|examples|readOnly|writeOnly|\")?[L.pattern]:void 0}case\"TemplateLiteral\":return h.checks.length===0?[`^${h.parts.map(ax).join(\"\")}$`]:void 0;case\"Union\":{if(h.types.length===0||h.checks.length>0||h.options?.mode===\"oneOf\")return;let C=[];for(let L=0;Le;function so(e){return e===void 0?void 0:{annotations:e}}function mp(e,t){let{references:r,representations:n}=OG([e],t);return{representation:n[0],references:r}}function OG(e,t){let r={},n=new Map,o=new Set,s=new Map,a=new Set;for(let h of e)m(h);let i=t?.referencePolicy??vG;for(let h of s.values())for(let b of h.values()){let T=i({ast:b.ast,occurrences:b.occurrences,identifier:b.identifier});if(T!==void 0){let C=T===b.identifier||!T.endsWith(\"_\")?\"_\":\"\";b.reference=u(T,b,C)}else b.isRecursive&&(b.reference=u(`${b.ast._tag}_`,b,\"\"))}return{representations:wn(e,h=>y(h)),references:r};function u(h,b,T=\"_\"){let C=h,L=0;for(;n.has(C);){if(n.get(C)===b)return C;C=`${h}${T}${++L}`}return n.set(C,b),C}function f(h,b,T){let C=b.fallback;return C!==void 0?XE(h)===C?h:to(h,{[vi]:C}):T===b.identifier?h:to(h,{identifier:T})}function p(h,b){if(!Object.hasOwn(r,h)&&!o.has(h)){o.add(h);let T=S(b);o.delete(h),M(r,h,T)}return{_tag:\"Reference\",$ref:h}}function d(h){let b=bu(h),T=Su(b),C=iu(b),L=C===void 0?(b!==h?iu(h):void 0)??XE(b):void 0;L!==void 0&&(C=`${L}Encoded`);let ee=s.get(T);ee===void 0&&(ee=new Map,s.set(T,ee));let ye=ee.get(C);return ye===void 0&&(ye={ast:T,identifier:C,fallback:L,occurrences:0,isRecursive:!1,reference:void 0},ee.set(C,ye)),ye}function m(h){let b=d(h),T=b.ast;if(b.occurrences++,a.has(b)){b.isRecursive=!0;return}if(!(b.occurrences>1)){switch(a.add(b),x(T.checks),T._tag){case\"Declaration\":case\"Arrays\":case\"Objects\":case\"Union\":T.recur(C=>(m(C),C));break;case\"TemplateLiteral\":T.parts.forEach(m);break;case\"Suspend\":m(T.thunk());break}a.delete(b)}}function x(h){h?.forEach(b=>{b.annotations?.representation?.schemas?.forEach(T=>m(yt(T))),b._tag===\"FilterGroup\"&&x(b.checks)})}function y(h){let b=d(h),T=b.ast,C=b.reference;if(C!==void 0){let L=b.identifier===void 0?T:f(T,b,C);return p(C,L)}return S(T)}function S(h){let b=w(h.checks);switch(h._tag){case\"Declaration\":return{_tag:\"Declaration\",typeParameters:h.typeParameters.map(T=>y(T)),checks:b,...U(h.annotations)};case\"Null\":case\"Undefined\":case\"Void\":case\"Never\":case\"Unknown\":case\"Any\":case\"String\":case\"Boolean\":case\"Number\":case\"BigInt\":case\"Symbol\":case\"ObjectKeyword\":return{_tag:h._tag,checks:b,...so(h.annotations)};case\"Literal\":return{_tag:\"Literal\",literal:h.literal,checks:b,...so(h.annotations)};case\"UniqueSymbol\":return{_tag:\"UniqueSymbol\",symbol:h.symbol,checks:b,...so(h.annotations)};case\"Enum\":return{_tag:\"Enum\",enums:h.enums,checks:b,...so(h.annotations)};case\"TemplateLiteral\":return{_tag:\"TemplateLiteral\",parts:h.parts.map(T=>y(T)),checks:b,...so(h.annotations)};case\"Arrays\":return{_tag:\"Arrays\",elements:h.elements.map(T=>{let C=bu(T),L=C.context?.annotations;return{isOptional:Qt(C),type:y(T),...so(L)}}),rest:h.rest.map(T=>y(T)),checks:b,...so(h.annotations)};case\"Objects\":return{_tag:\"Objects\",propertySignatures:h.propertySignatures.map(T=>{let C=bu(T.type),L=C.context?.annotations;return{name:T.name,type:y(T.type),isOptional:Qt(C),isMutable:fF(C),...so(L)}}),indexSignatures:h.indexSignatures.map(T=>({parameter:y(T.parameter),type:y(T.type)})),checks:b,...so(h.annotations)};case\"Union\":return{_tag:\"Union\",types:h.types.map(T=>y(T)),...h.options===void 0?{}:{options:h.options},checks:b,...so(h.annotations)};case\"Suspend\":return{_tag:\"Suspend\",checks:[],thunk:y(h.thunk()),...so(h.annotations)}}}function w(h){return h?.map(E)??[]}function E(h){switch(h._tag){case\"Filter\":return{_tag:\"Filter\",aborted:h.aborted,..._(h.annotations)};case\"FilterGroup\":return{_tag:\"FilterGroup\",checks:wn(h.checks,E),..._(h.annotations)}}}function U(h){if(h===void 0)return;let{representation:b,...T}=h;return{...b===void 0?void 0:{representation:b},...Object.keys(T).length===0?void 0:{annotations:T}}}function _(h){if(h===void 0)return;let{representation:b,...T}=h,C=b===void 0?void 0:b.schemas===void 0?b:{...b,schemas:b.schemas.map(L=>y(yt(L)))};return{...C===void 0?void 0:{representation:C},...Object.keys(T).length===0?void 0:{annotations:T}}}}function hN(e){return Ve(e)?e.value:{issues:[{message:cb(e.cause)}]}}function gN(e,t){let r=nt(e),n={errors:\"all\",...t?.parseOptions},o=OP(t),s=a=>{let i=new Qa(\"sync\"),c=Nl(Zb(r(a,n),{onFailure:o,onSuccess:f=>({value:f})}),{scheduler:i});c.currentDispatcher?.flush();let u=c.pollUnsafe();return u?hN(u):new Promise(f=>{c.addObserver(p=>{f(hN(p))})})};if(Object.hasOwn(e,\"~standard\")){let a=e;return\"validate\"in a[\"~standard\"]||Object.assign(a[\"~standard\"],{validate:s}),a}return Object.assign(e,{\"~standard\":{version:1,vendor:\"effect\",validate:s}})}function _G(e){let t=mp(Cu(e));return ix(t)}function xN(e,t){let r=_G(e);if(t===\"draft-2020-12\"){let n=r.schema;return Object.keys(r.definitions).length>0&&(n.$defs=r.definitions),n}else if(t===\"draft-07\"){let n=GF(r),o=n.schema;return Object.keys(n.definitions).length>0&&(o.definitions=n.definitions),o}throw new globalThis.Error(`Unsupported target: ${t}`)}function yN(e){let t={input(r){return xN(e.ast,r.target)},output(r){return xN(yt(e.ast),r.target)}};if(Object.hasOwn(e,\"~standard\")){let r=e;return\"jsonSchema\"in r[\"~standard\"]||Object.assign(r[\"~standard\"],{jsonSchema:t}),r}return Object.assign(e,{\"~standard\":{version:1,vendor:\"effect\",jsonSchema:t}})}function bN(e,t){let r=[];return ST(e,t,\"\",r),r}function ST(e,t,r,n){if(!Object.is(e,t)){if(Array.isArray(e)&&Array.isArray(t)){let o=e.length,s=t.length,a=Math.min(o,s);for(let i=0;i=s;i--)n.push({op:\"remove\",path:`${r}/${i}`});for(let i=o;ii)throw new Error(`Array index out of bounds at \"${t.path}\".`);let c=o.slice();return t.op===\"add\"?c.splice(a,0,t.value):t.op===\"remove\"?c.splice(a,1):c[a]=t.value,SN(s,c)}if(cx(o)){if(t.op!==\"add\"&&!Object.hasOwn(o,n))throw new Error(`Property \"${n}\" does not exist at \"${t.path}\".`);let a={...o};return t.op===\"remove\"?delete a[n]:M(a,n,t.value),SN(s,a)}throw new Error(`Cannot ${t.op} at \"${t.path}\" (parent not found or not a container).`)}function FG(e,t){let r=PG(t);if(r.length===0)return null;let n=r[r.length-1],o=[],s=e;for(let a=0;a=s.length)return null;o.push({container:s,token:c}),s=s[c];continue}if(cx(s)){if(!Object.hasOwn(s,i))return null;o.push({container:s,token:i}),s=s[i];continue}return null}return{stack:o,parent:s,lastToken:n}}function SN(e,t){let r=t;for(let n=e.length-1;n>=0;n--){let{container:o,token:s}=e[n];if(Array.isArray(o)){let a=o.slice();a[s]=r,r=a}else{let a={...o};M(a,s,r),r=a}}return r}function wN(e){let t=rx(e),r=Xh(t),n=Qh(t);return{empty:[],diff:(o,s)=>bN(r(o),r(s)),combine:(o,s)=>[...o,...s],patch:(o,s)=>{let a=r(o),i=EN(s,a);return Object.is(i,a)?o:n(i)}}}function CN(e,t){let r=iu(e.ast)??eI(e.ast),n=MF(ox(e));return o=>n(o).pipe(j(s=>UG(s,{rootName:r,...t})))}function UG(e,t){let r=t.rootName??\"root\",n=t.arrayItemName??\"item\",o=t.pretty??!0,s=t.indent??\" \",a=t.sortKeys??!0,i=new Set,c=[];return f(r,e,0),c.join(o?`\n`:\"\");function u(p,d){c.push(o?s.repeat(p)+d:d)}function f(p,d,m,x){let{attrs:y,safe:S}=Au.tagInfo(p,x);if(d===void 0)u(m,`<${S}${y}/>`);else if(typeof d==\"string\")u(m,`<${S}${y}>${Au.escapeText(d)}`);else if(typeof d!=\"object\"||d===null)u(m,`<${S}${y}>${Au.escapeText(F(d))}`);else{if(i.has(d))throw new globalThis.Error(\"Cycle detected while serializing to XML.\",{cause:d});i.add(d);try{if(globalThis.Array.isArray(d)){if(d.length===0){u(m,`<${S}${y}/>`);return}u(m,`<${S}${y}>`);for(let U of d)f(n,U,m+1);u(m,``);return}let w=d,E=Object.keys(w);if(a&&E.sort(),E.length===0){u(m,`<${S}${y}/>`);return}u(m,`<${S}${y}>`);for(let U of E)f(Au.parseTagName(U).safe,w[U],m+1,U);u(m,``)}finally{i.delete(d)}}}}var Au={escapeText(e){return e.replace(/&/g,\"&\").replace(//g,\">\")},escapeAttribute(e){return e.replace(/&/g,\"&\").replace(/\"/g,\""\").replace(//g,\">\")},parseTagName(e){let t=e,r=e;return/^[A-Za-z_]/.test(r)||(r=\"_\"+r),r=r.replace(/[^A-Za-z0-9._-]/g,\"_\"),/^xml/i.test(r)&&(r=\"_\"+r),{safe:r,changed:r!==t}},tagInfo(e,t){let{changed:r,safe:n}=Au.parseTagName(e),s=r||t&&t!==e?` data-name=\"${Au.escapeAttribute(t??e)}\"`:\"\";return{safe:n,attrs:s}}};var bT=Vr(e=>Sa(e));function Sa(e){let t=As(e)?.toEquivalence;if(t)return t(gu(e)?e.typeParameters.map(Sa):[]);switch(e._tag){case\"Never\":return Ja();case\"Declaration\":return LG(e);case\"Null\":case\"Undefined\":case\"Void\":case\"Unknown\":case\"Any\":case\"String\":case\"Number\":case\"Boolean\":case\"BigInt\":case\"Symbol\":case\"Literal\":case\"UniqueSymbol\":case\"ObjectKeyword\":case\"Enum\":case\"TemplateLiteral\":return J;case\"Arrays\":{let r=e.elements.map(Sa),n=e.rest.map(Sa),[o,...s]=n,a=s.length;return nr((i,c)=>{if(!Array.isArray(i)||!Array.isArray(c))return!1;let u=i.length;if(u!==c.length)return!1;let f=0;for(;f0){for(;fSa(o.type)),n=e.indexSignatures.map(o=>Sa(o.type));return nr((o,s)=>{if(!fr(o)||!fr(s))return!1;for(let a=0;a[Ui(s),Sa(e.types[a])]);return nr((s,a)=>{let i=n(s,!1);for(let c=0;c(r??=bT(e.thunk()))(n,o))}}}function LG(e){let t=e.annotations?.representation;if(t===void 0)return J;switch(t.id){case\"effect/schema/Option\":{let[r]=$i(e);return(n,o)=>n._tag===o._tag&&(n._tag===\"None\"||r(n.value,o.value))}case\"effect/schema/Result\":{let[r,n]=$i(e);return(o,s)=>o._tag===s._tag&&(o._tag===\"Success\"?r(o.success,s.success):n(o.failure,s.failure))}case\"effect/schema/CauseReason\":{let[r,n]=$i(e);return kN(r,n)}case\"effect/schema/Cause\":{let[r,n]=$i(e);return AN(r,n)}case\"effect/schema/Exit\":{let[r,n,o]=$i(e),s=AN(n,o);return(a,i)=>a._tag===i._tag&&(a._tag===\"Success\"?r(a.value,i.value):s(a.cause,i.cause))}case\"effect/schema/ReadonlyMap\":{let[r,n]=$i(e);return ld(r,n)}case\"effect/schema/ReadonlySet\":return wf($i(e)[0]);case\"effect/schema/RegExp\":return(r,n)=>r.source===n.source&&r.flags===n.flags;case\"effect/schema/URL\":return(r,n)=>r.toString()===n.toString();default:return J}}function $i(e){return e.typeParameters.map(Sa)}function kN(e,t){return(r,n)=>{if(r._tag!==n._tag)return!1;switch(r._tag){case\"Fail\":return e(r.error,n.error);case\"Die\":return t(r.defect,n.defect);case\"Interrupt\":return r.fiberId===n.fiberId}}}function AN(e,t){let r=Sd(kN(e,t));return(n,o)=>r(n.reasons,o.reasons)}function ON(e,t){return r=>{switch(r._tag){case\"Fail\":return`Fail(${e(r.error)})`;case\"Die\":return`Die(${t(r.defect)})`;case\"Interrupt\":return\"Interrupt\"}}}function vN(e,t){let r=ON(e,t);return n=>`Cause([${n.reasons.map(r).join(\", \")}])`}function _N(e,t){let r=new Map;return n(e);function n(a){let i=r.get(a);return i===void 0&&(i=o(a),r.set(a,i)),i}function o(a){let i=As(a)?.toFormatter;if(typeof i==\"function\")return i(gu(a)?a.typeParameters.map(n):[]);if(t?.onBefore){let c=t.onBefore(a,n);if(c!==void 0)return c}return s(a)}function s(a){switch(a._tag){default:return F;case\"Never\":return()=>\"never\";case\"Void\":return()=>\"void\";case\"Declaration\":{let i=a.annotations?.representation;if(i===void 0)return F;let c=a.typeParameters.map(n);switch(i.id){case\"effect/schema/Option\":{let[u]=c;return Wt({onNone:()=>\"none()\",onSome:f=>`some(${u(f)})`})}case\"effect/schema/Result\":{let[u,f]=c;return wr({onSuccess:p=>`success(${u(p)})`,onFailure:p=>`failure(${f(p)})`})}case\"effect/schema/CauseReason\":return ON(c[0],c[1]);case\"effect/schema/Cause\":return vN(c[0],c[1]);case\"effect/schema/Exit\":{let[u,f,p]=c,d=vN(f,p);return m=>{switch(m._tag){case\"Success\":return`Exit.Success(${u(m.value)})`;case\"Failure\":return`Exit.Failure(${d(m.cause)})`}}}case\"effect/schema/ReadonlyMap\":{let[u,f]=c;return p=>{let d=p.size;if(d===0)return\"ReadonlyMap(0) {}\";let m=globalThis.Array.from(p.entries()).sort().map(([x,y])=>`${u(x)} => ${f(y)}`);return`ReadonlyMap(${d}) { ${m.join(\", \")} }`}}case\"effect/schema/HashMap\":{let[u,f]=c;return p=>{let d=VR(p);if(d===0)return\"HashMap(0) {}\";let m=zl(p).sort().map(([x,y])=>`${u(x)} => ${f(y)}`);return`HashMap(${d}) { ${m.join(\", \")} }`}}case\"effect/schema/ReadonlySet\":{let[u]=c;return f=>{let p=f.size;if(p===0)return\"ReadonlySet(0) {}\";let d=globalThis.Array.from(f.values()).sort().map(m=>`${u(m)}`);return`ReadonlySet(${p}) { ${d.join(\", \")} }`}}case\"effect/schema/HashSet\":{let[u]=c;return f=>{let p=eP(f);if(p===0)return\"HashSet(0) {}\";let d=globalThis.Array.from(f).sort().map(m=>`${u(m)}`);return`HashSet(${p}) { ${d.join(\", \")} }`}}case\"effect/schema/Chunk\":{let[u]=c;return f=>{let p=IR(f);if(p===0)return\"Chunk(0) {}\";let d=globalThis.Array.from(f).sort().map(m=>`${u(m)}`);return`Chunk(${p}) { ${d.join(\", \")} }`}}default:return F}}case\"Arrays\":{let i=a.elements.map(d=>n(d)),c=a.rest.map(n),[u,...f]=c,p=f.length;return d=>{let m=[],x=0;for(;x0){for(;xn(u.type)),c=a.indexSignatures.map(u=>n(u.type));return a.propertySignatures.length===0&&a.indexSignatures.length===0?F:u=>{let f=[],p=new Set;for(let d=0;d0?\"{ \"+f.join(\", \")+\" }\":\"{}\"}}case\"Union\":{let i=yt(a).types,c=sp(i),u=i.map((f,p)=>[Ui(f),n(a.types[p])]);return f=>{let p=c(f,!1);for(let d=0;d(i??=n(a.thunk()))(c)}}}}var ET=l(2,(e,t)=>wT(e,(r,n)=>PN(t,r)?[r,n]:void 0)),IT=l(2,(e,t)=>wT(e,(r,n)=>PN(t,r)?void 0:[r,n])),RN=l(2,(e,t)=>({...e,...t}));var TT=l(2,(e,t)=>wT(e,(r,n)=>[Object.hasOwn(t,r)?t[r]:r,n]));var mr=e=>e;function PN(e,t){return e.some(r=>r===t||typeof r==\"number\"&&String(r)===t)}function wT(e,t){let r={};for(let n of Reflect.ownKeys(e)){if(!Object.prototype.propertyIsEnumerable.call(e,n))continue;let o=t(n,e[n]);if(o){let[s,a]=o;M(r,s,a)}}return r}function NN(e,t){return ao(ku(\"Iso\",e,t))}function MN(e,t){return ao(ku(\"Lens\",e,t))}function ku(e,t,r){return[{_tag:\"PrimitiveNode\",kind:e,get:t,set:r}]}var jG={kind:\"Iso\",get:R,set:R},ux=class{_tag=\"PathNode\";kind=\"Lens\";path;get;set;constructor(t){this.path=t,this.get=r=>{let n=r;for(let o=0;o{let o=fx(n),s=o,a=0;for(;agF(t,r)}};function Bi(e,t){if(e.length===0)return t;if(t.length===0)return e;let r=e.slice();for(let n=0;nr)}modify(t){return r=>Lf(Lt(this.getResult(r),n=>this.replaceResult(t(n),r)),()=>r)}compose(t){return ao(Bi(this.node,t.node))}key(t){return ao(Bi(this.node,[new ux([t])]))}optionalKey(t){return ao(Bi(this.node,ku(\"Lens\",r=>r[t],(r,n)=>{let o=fx(n);return r===void 0?Array.isArray(o)&&(typeof t==\"number\"||typeof t==\"string\"&&Cc(t))?o.splice(Number(t),1):delete o[t]:M(o,t,r),o})))}check(...t){return ao(Bi(this.node,[new hp(t)]))}refine(t,r){return ao(Bi(this.node,[new hp([jh(t,r)])]))}tag(t){let r=W(new de({expected:`${JSON.stringify(t)} tag`}));return ao(Bi(this.node,ku(\"Prism\",n=>n._tag===t?B(n):r,R)))}at(t,...r){let n=W(new ut([t],new ma(void 0)));return ao(Bi(this.node,ku(\"Optional\",o=>Object.hasOwn(o,t)?B(o[t]):n,(o,s)=>{if(Object.hasOwn(s,t)){let a=fx(s);return M(a,t,o),B(a)}else return n})))}pick(t){return this.compose(MN(ET(t),(r,n)=>({...IT(n,t),...r})))}omit(t){return this.compose(MN(IT(t),(r,n)=>({...ET(n,t),...r})))}notUndefined(){return this.refine(Io,{expected:\"a value other than `undefined`\"})}forEach(t){let r=t(lx());return zG(n=>Tr(this.getResult(n),o=>{let s=[];for(let a=0;aLt(this.getResult(o),s=>{let a=[];for(let c=0;cLf(Lt(this.getResult(r),n=>this.replaceResult(n.map(t),r)),()=>r)}},AT=class extends vu{get;set;constructor(t,r,n){super(t,o=>B(r(o)),o=>B(n(o))),this.get=r,this.set=n}replace(t,r){return this.set(t)}modify(t){return r=>this.set(t(this.get(r)))}},kT=class extends vu{get;constructor(t,r,n){super(t,o=>B(r(o)),(o,s)=>B(n(o,s))),this.get=r,this.replace=n}modify(t){return r=>this.replace(t(this.get(r)),r)}},vT=class extends vu{set;constructor(t,r,n){super(t,r,(o,s)=>B(n(o))),this.set=n}replace(t,r){return this.set(t)}modify(t){return r=>Lf(Tr(this.getResult(r),n=>this.set(t(n))),()=>r)}};function ao(e){let t=e[0]??jG;if(e.length>1){let r=e.reduce((n,o)=>WG(n,o.kind),\"Iso\");t={kind:r,get:qG(e,r),set:HG(e,r)}}switch(t.kind){case\"Iso\":return new AT(e,t.get,t.set);case\"Lens\":return new kT(e,t.get,t.set);case\"Prism\":return new vT(e,t.get,t.set);case\"Optional\":return new vu(e,t.get,t.set)}}function fx(e){if(Array.isArray(e))return e.slice();if(typeof e==\"object\"&&e!==null){let t=Object.getPrototypeOf(e);if(t!==Object.prototype&&t!==null)throw new Error(\"Cannot clone object with non-Object constructor or null prototype\");return{...e}}return e}function qG(e,t){return r=>{for(let n=0;n{for(let n=e.length-1;n>=0;n--)r=e[n].set(r);return r}:(r,n)=>{let o=e.length,s=new Array(o);for(let a=0;a=0;a--){let i=e[a];if(FN(i.kind))r=i.set(r);else if(i.kind===\"Lens\")r=i.set(r,s[a]);else{let c=i.set(r,s[a]);if(ce(c))return c;r=c.success}}return t===\"Optional\"?B(r):r}}function OT(e){return e===\"Prism\"||e===\"Optional\"}function FN(e){return e===\"Iso\"||e===\"Prism\"}function WG(e,t){return e===\"Iso\"?t:t===\"Iso\"||e===t?e:\"Optional\"}var JG=ao([]);function lx(){return JG}function DN(e){let t=nx(e);return NN(Xh(t),Qh(t))}function LN(e){return lx()}function $N(e){return lx()}var Ou=\"~effect/Schema/SchemaError\";function BN(e){return k(e,Ou)&&e[Ou]===Ou}var yn=\"~effect/net/NetAddress\";var io=e=>e.value,co=e=>e,vs=e=>e.bytes,ji=class extends Fr(\"NetAddressError\"){},_u=e=>k(e,yn),hr=e=>_u(e)&&e._tag===\"Ipv4Address\",Yo=e=>_u(e)&&e._tag===\"Ipv6Address\",jN=e=>hr(e)||Yo(e),Ru=e=>hr(e)?32:128,Pu=e=>_u(e)&&e._tag===\"MacAddress\",gp=e=>_u(e)&&e._tag===\"InetAddressV4\",yp=e=>_u(e)&&e._tag===\"InetAddressV6\",_T=e=>gp(e)||yp(e),px=e=>_u(e)&&e._tag===\"UnixPathAddress\",zN=e=>_T(e)||px(e),GG={_tag:\"Ipv4Address\",[yn]:yn,[ae](e){return hr(e)&&io(this)===io(e)},[se](){return qt(le(QG,io(this)|0))},toString(){return uo(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},VG={_tag:\"Ipv6Address\",[yn]:yn,[ae](e){if(!Yo(e))return!1;let t=co(this),r=co(e);return t.w0===r.w0&&t.w1===r.w1&&t.w2===r.w2&&t.w3===r.w3},[se](){let e=co(this),t=le(XG,e.w0|0);return t=le(t,e.w1|0),t=le(t,e.w2|0),qt(le(t,e.w3|0))},toString(){return uo(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},ZG={_tag:\"MacAddress\",[yn]:yn,[ae](e){return Pu(e)&&YG(vs(this),vs(e))},[se](){return le(e3,To(vs(this)))},toString(){return PT(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},YG=(e,t)=>{if(e.length!==t.length)return!1;for(let r=0;r{let t=Object.create(GG);return t.value=e>>>0,Object.freeze(t)},r3=(e,t,r,n)=>{let o=Object.create(VG);return o.w0=e>>>0,o.w1=t>>>0,o.w2=r>>>0,o.w3=n>>>0,Object.freeze(o)},n3=(e,t,r,n)=>(e<<24|t<<16|r<<8|n)>>>0;var xp=e=>[e>>>24,e>>>16&255,e>>>8&255,e&255],o3=e=>{let t=Object.assign(Object.create(ZG),{bytes:new Uint8Array(e)});return Object.freeze(t)};var St=(e,t)=>W(new ji({input:e,message:t})),s3=e=>e.every(t=>Number.isInteger(t)&&t>=0&&t<=255)?B(t3(n3(e[0],e[1],e[2],e[3]))):St(e,\"octets must be integers from 0 through 255\"),a3=e=>e.every(t=>Number.isInteger(t)&&t>=0&&t<=65535)?B(r3(e[0]*65536+e[1],e[2]*65536+e[3],e[4]*65536+e[5],e[6]*65536+e[7])):St(e,\"segments must be integers from 0 through 65535\");var qN=e=>/^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/.test(e)?B(o3(new Uint8Array(e.split(\":\").map(t=>Number.parseInt(t,16))))):St(e,\"expected six two-digit hexadecimal octets separated by colons\");var Mu=e=>{let t=e.split(\".\");if(t.length!==4||t.some(n=>!/^\\d{1,3}$/.test(n)))return St(e,\"expected exactly four decimal octets\");if(t.some(n=>n.length>1&&n[0]===\"0\"))return St(e,\"leading zeroes are not allowed\");let r=t.map(Number);return r.some(n=>n>255)?St(e,\"octets must be at most 255\"):s3([r[0],r[1],r[2],r[3]])},i3=e=>{if(e.includes(\"[\")||e.includes(\"]\")||e.includes(\"%\"))return St(e,\"brackets and zone identifiers are not valid in a bare IPv6 address\");let t=e.split(\"::\");if(t.length>2)return St(e,\"only one compression marker is allowed\");let r=t[0]===\"\"?[]:t[0].split(\":\"),n=t.length===2&&t[1]!==\"\"?t[1].split(\":\"):[];if(r.some(f=>f===\"\")||n.some(f=>f===\"\"))return St(e,\"empty segments are only valid in the compression marker\");let o=n.length>0?n[n.length-1]:r.length>0?r[r.length-1]:\"\",s;if(o.includes(\".\")){if(t.length===2&&n.length===0)return St(e,\"embedded IPv4 syntax must be trailing\");let f=Tr(Mu(o),p=>{let d=io(p);return[d>>>16,d&65535]});if(ce(f))return f;s=f.success,n.length>0?n.pop():r.pop()}let a=r.length+n.length+(s?2:0);if(t.length===1?a!==8:a>=8)return St(e,t.length===1?\"expected eight segments\":\"compression must replace at least one segment\");let i=f=>/^[0-9a-fA-F]{1,4}$/.test(f)?Number.parseInt(f,16):void 0,c=r.map(i),u=n.map(i);return c.some(f=>f===void 0)||u.some(f=>f===void 0)?St(e,\"segments must contain one through four hexadecimal digits\"):B([...c,...Array(8-a).fill(0),...u,...s??[]])},Sp=e=>Lt(i3(e),t=>a3(t)),bp=e=>e.includes(\":\")?Sp(e):Mu(e);var RT=e=>xp(io(e)),c3=e=>{let{w0:t,w1:r,w2:n,w3:o}=co(e);return[t>>>16,t&65535,r>>>16,r&65535,n>>>16,n&65535,o>>>16,o&65535]},HN=e=>{let{w0:t,w1:r,w2:n,w3:o}=co(e);return[...xp(t),...xp(r),...xp(n),...xp(o)]};var PT=e=>Array.from(vs(e),t=>t.toString(16).padStart(2,\"0\")).join(\":\"),dx=e=>vs(e).every(t=>t===255),u3=e=>(vs(e)[0]&1)!==0,mx=e=>(vs(e)[0]&1)===0,MT=e=>(vs(e)[0]&2)!==0,FT=e=>(vs(e)[0]&2)===0;var uo=e=>{if(hr(e))return RT(e).join(\".\");let t=c3(e);if(f3(e))return`::ffff:${t[6]>>8}.${t[6]&255}.${t[7]>>8}.${t[7]&255}`;let r=-1,n=0,o=-1;for(let i=0;i<=8;i++)i<8&&t[i]===0?o===-1&&(o=i):o!==-1&&(i-o>n&&(r=o,n=i-o),o=-1);if(n<2)return t.map(i=>i.toString(16)).join(\":\");let s=t.slice(0,r).map(i=>i.toString(16)).join(\":\"),a=t.slice(r+n).map(i=>i.toString(16)).join(\":\");return`${s}::${a}`},hx=e=>{if(hr(e))return io(e)===0;let{w0:t,w1:r,w2:n,w3:o}=co(e);return t===0&&r===0&&n===0&&o===0},NT=e=>{if(hr(e))return io(e)>>>24===127;let{w0:t,w1:r,w2:n,w3:o}=co(e);return t===0&&r===0&&n===0&&o===1},Fu=e=>Pu(e)?u3(e):hr(e)?io(e)>>>28===14:co(e).w0>>>24===255,xx=e=>Pu(e)?dx(e):io(e)===4294967295,UT=e=>Pu(e)?mx(e):!Fu(e)&&!hx(e)&&(!hr(e)||!xx(e)),DT=e=>hr(e)?io(e)>>>16===43518:(co(e).w0>>>16&65472)===65152,LT=e=>{let t=io(e);return t>>>24===10||(t>>>16&65520)===44048||t>>>16===49320},$T=e=>(co(e).w0>>>24&254)===252,f3=e=>{let{w0:t,w1:r,w2:n}=co(e);return t===0&&r===0&&n===65535};var l3={_tag:\"InetAddressV4\",[yn]:yn,[ae](e){return gp(e)&&this.port===e.port&&J(this.address,e.address)},[se](){return le(H(this.address),It(this.port))},toString(){return gx(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},p3={_tag:\"InetAddressV6\",[yn]:yn,[ae](e){return yp(e)&&this.port===e.port&&this.scopeId===e.scopeId&&J(this.address,e.address)},[se](){return le(le(H(this.address),It(this.port)),It(this.scopeId))},toString(){return gx(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},WN=e=>Number.isInteger(e)&&e>=0&&e<=65535,d3=(e,t)=>{if(!WN(t))return St(e,\"port must be an integer from 0 through 65535\");let r=Object.create(l3);return r.address=e,r.port=t,B(Object.freeze(r))},m3=(e,t,r)=>{if(!WN(t))return St(e,\"port must be an integer from 0 through 65535\");let n=r?.scopeId??0;if(!Number.isInteger(n)||n<0||n>4294967295)return St(e,\"scopeId must be an unsigned 32-bit integer\");let o=Object.create(p3);return o.address=e,o.port=t,o.scopeId=n,B(Object.freeze(o))};var JN=e=>{let t,r,n=0,o=e.startsWith(\"[\");if(o){let s=e.indexOf(\"]\");if(s<0||e[s+1]!==\":\"||e.indexOf(\"]\",s+1)!==-1)return St(e,\"expected [IPv6]:port\");t=e.slice(1,s),r=e.slice(s+2);let a=t.indexOf(\"%\");if(a!==-1){let i=t.slice(a+1);if(t.indexOf(\"%\",a+1)!==-1||!/^\\d+$/.test(i))return St(e,\"scope identifier must be decimal\");if(n=Number(i),!Number.isInteger(n)||n>4294967295)return St(e,\"scope identifier must be an unsigned 32-bit integer\");t=t.slice(0,a)}}else{let s=e.lastIndexOf(\":\");if(s<0)return St(e,\"expected host:port or [IPv6]:port\");if(e.indexOf(\":\")!==s)return St(e,\"IPv6 addresses must be bracketed\");t=e.slice(0,s),r=e.slice(s+1)}return/^(0|[1-9]\\d*)$/.test(r)?Lt(bp(t),s=>o!==Yo(s)?St(e,\"only IPv6 addresses use brackets\"):Yo(s)?m3(s,Number(r),{scopeId:n}):d3(s,Number(r))):St(e,\"port must be an unpadded decimal integer\")};var h3=e=>uo(e.address)+(yp(e)&&e.scopeId!==0?`%${e.scopeId}`:\"\");var gx=e=>{let t=h3(e);return gp(e)?`${t}:${e.port}`:`[${t}]:${e.port}`};var x3={_tag:\"UnixPathAddress\",[yn]:yn,[ae](e){return px(e)&&this.path===e.path},[se](){return le(Re(\"UnixPathAddress\"),Re(this.path))},toString(){return this.path},toJSON(){return this.toString()},[Te](){return this.toJSON()}},KN=e=>{let t=Object.create(x3);return t.path=e,Object.freeze(t)};var jT=\"~effect/net/IpInterface\",GN=e=>Ep(e)&&hr(e.address),VN=e=>Ep(e)&&Yo(e.address),Ep=e=>k(e,jT),g3={_tag:\"IpInterface\",[jT]:jT,[ae](e){return Ep(e)&&this.prefixLength===e.prefixLength&&J(this.address,e.address)},[se](){return le(H(this.address),It(this.prefixLength))},toString(){return Tp(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},zT=(e,t)=>W(new ji({input:e,message:t})),Ip=(e,t)=>{let r=Ru(e);if(!Number.isInteger(t)||t<0||t>r)return zT(e,`prefix length must be an integer from 0 through ${r}`);let n=Object.assign(Object.create(g3),{address:e,prefixLength:t});return B(Object.freeze(n))},qT=(e,t)=>{let r=e.indexOf(\"/\");if(r===-1&&t?.prefix!==\"required\")return B({address:e,prefixLength:void 0});if(r<=0||r!==e.lastIndexOf(\"/\")||r===e.length-1)return zT(e,\"expected an address and prefix length separated by one slash\");let n=e.slice(r+1);return/^(0|[1-9][0-9]*)$/.test(n)?B({address:e.slice(0,r),prefixLength:Number(n)}):zT(e,\"prefix length must be an unpadded ASCII decimal integer\")},yx=(e,t)=>Lt(qT(e,t),r=>Lt(Mu(r.address),n=>Ip(n,r.prefixLength??Ru(n)))),Sx=(e,t)=>Lt(qT(e,t),r=>Lt(Sp(r.address),n=>Ip(n,r.prefixLength??Ru(n)))),bx=(e,t)=>Lt(qT(e,t),r=>Lt(bp(r.address),n=>Ip(n,r.prefixLength??Ru(n))));var Tp=e=>`${uo(e.address)}/${e.prefixLength}`;var HT=\"~effect/net/IpNetwork\",YN=e=>wp(e)&&hr(e.address),QN=e=>wp(e)&&Yo(e.address),wp=e=>k(e,HT),y3={_tag:\"IpNetwork\",[HT]:HT,[ae](e){return wp(e)&&this.prefixLength===e.prefixLength&&J(this.address,e.address)},[se](){return le(H(this.address),It(this.prefixLength))},toString(){return Cp(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},S3=e=>{let t=Object.assign(Object.create(y3),{address:e.address,prefixLength:e.prefixLength});return Object.freeze(t)},b3=e=>hr(e)?RT(e):HN(e);var E3=(e,t)=>{let r=Math.floor(t/8),n=t%8;return e.map((o,s)=>sLt(Ip(e,t),r=>{let n=b3(e),o=E3(n,t);return n.some((s,a)=>s!==o[a])?W(new ji({input:e,message:\"address has non-zero host bits\"})):B(S3(r))});var XN=e=>Lt(yx(e,{prefix:\"required\"}),t=>WT(t.address,t.prefixLength)),eU=e=>Lt(Sx(e,{prefix:\"required\"}),t=>WT(t.address,t.prefixLength)),tU=e=>Lt(bx(e,{prefix:\"required\"}),t=>WT(t.address,t.prefixLength));var Cp=e=>`${uo(e.address)}/${e.prefixLength}`;var Ap=tx;function qr(){return(e,t,r)=>q(new yu(e.map(op),n=>t(n.map(o=>q(o))),r))}function Be(e,t){return qr()([],()=>(r,n,o)=>e(r)?g(r):P(new ft(n,r,o)),t)}function T3(e){return e}function w3(e){return t=>t.annotate(e)}function C3(e){return t=>JT(JT(t).annotate(e))}function A3(e){return t=>t.rebuild(cp(t.ast,e))}function k3(e){return e}var Os=class extends Fr(\"SchemaError\"){[Ou]=Ou;constructor(t){let r=Co();Yr(0);try{super({issue:t})}finally{Yr(r)}}get message(){return _P(this.issue)}toString(){return`SchemaError(${this.message})`}};function hU(e){return BN(e)}function xU(e){return Ne(e)?YT(e):rt(e,t=>Is(()=>Ss(t,r=>new Os(r))))}function YT(e){return Ve(e)?e:sn(Ss(e.cause,t=>new Os(t)))}function gU(e,t){let r;for(let n of e.reasons){if(!na(n)||!hU(n.error))throw new globalThis.Error(t,{cause:e});r??=n.error}if(r===void 0)throw new globalThis.Error(t,{cause:e});return r}function yU(e){return ah(e).then(t=>{if(Ve(t))return t.value;throw gU(t.cause,\"Promise adapter can only reject schema errors\")})}function SU(e){let t=Wo(e);if(Ve(t))return t.value;throw gU(t.cause,\"Sync adapter can only throw schema errors\")}function v3(e,t){return gN(e,t)}function O3(e){return yN(e)}var _3=rT,R3=OF;function Ex(e,t){let r=nt(e,t);return(n,o)=>xU(r(n,o))}var P3=Ex;function bU(e,t){let r=_F(e,t);return(n,o)=>YT(r(n,o))}var M3=bU,F3=oT,N3=RF;function EU(e,t){let r=PF(e,t);return(n,o)=>Dd(r(n,o),s=>new Os(s))}var U3=EU;function IU(e,t){let r=Ex(e,t);return(n,o)=>yU(r(n,o))}var D3=IU;function TU(e,t){let r=Ex(e,t);return(n,o)=>SU(r(n,o))}var L3=TU;function Ix(e,t){let r=Di(e,t);return(n,o)=>xU(r(n,o))}var $3=Ix;function wU(e,t){let r=FF(e,t);return(n,o)=>YT(r(n,o))}var B3=wU,j3=sT,z3=NF;function CU(e,t){let r=UF(e,t);return(n,o)=>Dd(r(n,o),s=>new Os(s))}var q3=CU;function AU(e,t){let r=Ix(e,t);return(n,o)=>yU(r(n,o))}var H3=AU;function kU(e,t){let r=Ix(e,t);return(n,o)=>SU(r(n,o))}var W3=kU,q=oo;function Tx(e){return k(e,Ap)&&e[Ap]===Ap}var qi=mr(e=>q(Ni(e.ast),{schema:e})),J3=mr(e=>e.schema),Lu=mr(e=>{let t=Ax(e);return q(iF(e.ast),{schema:t})}),K3=mr(e=>e.schema.members[0]),G3=mr(e=>q(qI(e.ast),{schema:e})),V3=mr(e=>e.schema),Hr=mr(e=>q(yt(e.ast),{schema:e})),$u=mr(e=>q(hn(e.ast),{schema:e})),kp=\"~effect/Schema/flip\";function Z3(e){return k(e,kp)&&e[kp]===kp}function JT(e){return Z3(e)?e.schema.rebuild(mn(e.ast)):q(mn(e.ast),{[kp]:kp,schema:e})}function Ct(e){let t=q(new dn(e),{literal:e,transform(r){return t.pipe(G(Ct(r),{decode:oe(()=>r),encode:oe(()=>e)}))}});return t}function vU(e){return e.map(t=>Tx(t)?t.ast:new dn(t))}function Y3(e){return q(new _I(vU(e)),{parts:e})}function Q3(e){return q(BM(vU(e)),{parts:e})}function X3(e){return q(new $M(Object.keys(e).filter(t=>typeof e[e[t]]!=\"number\").map(t=>[t,e[t]])),{enums:e})}var eV=q(OI),tV=q(DM),vp=q(eo),wx=q(kI),QT=q(vI),K=q(pn),rV=K,Wi=q(RI),XT=q(qM),OU=q(HM),Bu=q(WM),nV=q(UM),oV=q(LM);function sV(e){return q(new zM(e))}function ew(e,t){return q(e,{fields:t,mapFields(r,n){let o=r(this.fields);return ew(Lh(o,n?.unsafePreserveChecks?this.ast.checks:void 0),o)}})}function Me(e){return ew(Lh(e,void 0),e)}function aV(e){return mr(t=>t.mapFields(RN(e)))}var iV=e=>typeof e==\"symbol\"?e:globalThis.String(e);function cV(e){return function(t){let r={},n=Object.create(null),o=Object.create(null),s=new Set;for(let a of Reflect.ownKeys(t.fields)){let i=$u(t.fields[a]),c=Object.hasOwn(e,a),u=c?e[a]:a,f=iV(u);if(s.has(f))throw new globalThis.Error(`Duplicate encoded keys: ${wo(u)}`);s.add(f),M(r,u,i),c&&(n[a]=u,o[u]=a)}return Me(r).pipe(G(t,Pe({decode:TT(o),encode:TT(n)})))}}function uV(e,t){return r=>{let n=Ya(r.fields,Hr),o=Me({...n,...e});return r.pipe(G(o,Pe({decode:s=>{let a={...s};for(let i in e){let c=t[i],u=c(s);be(u)&&M(a,i,u.value)}return a},encode:s=>{let a={...s};for(let i in e)delete a[i];return a}})))}}function _s(e,t){return q(uF(e.ast,t.ast),{key:e,value:t})}function fV(e,t){return q(ZM(e.ast,t.map(op)),{schema:e,records:t})}function _U(e,t){return q(e,{elements:t,mapElements(r,n){let o=r(this.elements);return _U(NI(o,n?.unsafePreserveChecks?this.ast.checks:void 0),o)}})}function Qo(e){return _U(NI(e),e)}function lV(e,t){return q(YM(e.ast,t.map(op)),{schema:e,rest:t})}var $e=mr(e=>q(new Pn(!1,[],[e.ast]),{value:e}));function Cx(e,t,r){return t!==void 0&&r!==void 0?e.check(iw(t,r)):t!==void 0?e.check(ZU(t)):r!==void 0?e.check(XU(r)):e}var tw=mr(e=>q(new Pn(!1,[e.ast],[e.ast]),{value:e}));function pV(e){let t=$e(e),r=$e(Hr(e)),n=G(Qo([vp]),Pe({decode:o=>[o],encode:([o])=>o}))(e);return q(it([n,t]).pipe(G(r)).ast,{from:it([e,t]),to:r})}function dV(e){return $e(e).check(uw())}var mV=mr(e=>q(QM(e.ast),{schema:e}));function RU(e,t){return q(e,{members:t,mapMembers(r,n){let o=r(this.members);return RU($h(o,this.ast.options,n?.unsafePreserveChecks?this.ast.checks:void 0),o)}})}function it(e,t){return RU($h(e,t,void 0),e)}function fo(e){let t=e.map(Ct);return q($h(t,void 0,void 0),{literals:e,members:t,mapMembers(r){return it(r(this.members))},pick(r){return fo(r)},transform(r){return it(t.map((n,o)=>n.transform(r[o])))}})}var rw=mr(e=>it([e,wx])),Ax=mr(e=>it([e,QT])),PU=mr(e=>it([e,wx,QT]));function MU(e){return q(new DI(()=>e().ast))}function hV(...e){return t=>t.check(...e)}function FU(e,t){return r=>q(Vo(r.ast,[jh(e,t)]),{schema:r})}function NU(e){return t=>q(t.ast,{schema:t,identifier:e})}function xV(e,t){return r=>(t.checks?r.check(...t.checks):r).pipe(NU(e))}function UU(e){return t=>q(sF(t.ast,new _h(e,R)),{schema:t})}function DU(e){return t=>q(aF(t.ast,new _h(R,e)),{schema:t})}function gV(e){return LU(e)}function LU(e){return t=>UU(ru(e))(t)}function yV(e){return $U(e)}function $U(e){return t=>DU(ru(e))(t)}function G(e,t){return r=>q(ga(r.ast,e.ast,t?Rh(t):lu()),{from:r,to:e})}function ju(){return(e,t)=>Oe()(e,{decode:t,encode:NP})}var BU=fo([\"true\",\"yes\",\"on\",\"1\",\"y\"]),jU=fo([\"false\",\"no\",\"off\",\"0\",\"n\"]),nw=fo([...BU.literals,...jU.literals]).pipe(G(XT,Pe({decode:e=>e===\"true\"||e===\"yes\"||e===\"on\"||e===\"1\"||e===\"y\",encode:e=>e?\"true\":\"false\"})));function SV(e){return t=>G(Hr(t),e)(t)}function ow(e,t){return r=>t?G(r,t)(e):G(r)(e)}function bV(e){return t=>G(t,e)($u(t))}function zU(e){return t=>q(cF(t.ast,e),{schema:t})}function qU(e){return rt(e,t=>Is(()=>Ss(t,r=>r.issue)))}function sw(e,t){let r=t?.encodingStrategy===\"omit\"?uI():jr();return n=>qi($u(n)).pipe(G(n,{decode:fI(qU(e)),encode:r}))}function EV(e,t){return r=>Hr(r).pipe(sw(e,t),ow(qi(r)))}function HU(e,t){let r=t?.encodingStrategy===\"omit\"?uI():jr();return n=>Lu($u(n)).pipe(G(n,{decode:fI(qU(e)),encode:r}))}function IV(e,t){return r=>Hr(r).pipe(HU(e,t),ow(Lu(r)))}function Hi(e){return Ct(e).pipe(zU(g(e)))}function TV(e){return Hi(e).pipe(sw(g(e),{encodingStrategy:\"omit\"}))}function kx(e,t){return Me({_tag:Hi(e),...t})}function WU(e){return t=>{let r={},n=[],o=new Set,s={},a=f=>p=>f.includes(p[e]);return i(t),Object.assign(t,{tag:e,cases:r,discriminants:n,isAnyOf:a,guards:s,match:c,matchOrElse:u});function i(f){let p=f.ast;if(MM(p)&&\"members\"in f&&globalThis.Array.isArray(f.members)&&f.members.every(Tx))return f.members.forEach(i);let d=hu(p);if(d.length>0){let m=d.find(x=>x.key===e)?.literal;if(id(m)){let x=typeof m==\"number\"?globalThis.String(m):m;if(o.has(x))throw new globalThis.Error(`Duplicate discriminant: ${globalThis.String(m)}`);o.add(x),n.push(m),M(r,m,f),M(s,m,rT(Hr(f)));return}}throw new globalThis.Error(\"No literal or unique symbol found\")}function c(){if(arguments.length===1){let x=arguments[0];return function(y){let S=y[e];return(Object.hasOwn(x,S)?x[S]:void 0)(y)}}let f=arguments[0],p=arguments[1],d=f[e];return(Object.hasOwn(p,d)?p[d]:void 0)(f)}function u(){if(arguments.length===2){let y=arguments[0],S=arguments[1];return function(w){let E=w[e];return(Object.hasOwn(y,E)?y[E]??S:S)(w)}}let f=arguments[0],p=arguments[1],d=arguments[2],m=f[e];return(Object.hasOwn(p,m)?p[m]??d:d)(f)}}}function wV(e){let t={},r=[];for(let u of Object.keys(e)){let f=kx(u,e[u]);M(t,u,f),r.push(f)}let n=it(r),{tag:o,guards:s,isAnyOf:a,match:i,matchOrElse:c}=WU(\"_tag\")(n);return q(n.ast,{tag:o,cases:t,isAnyOf:a,guards:s,match:i,matchOrElse:c})}function CV(){return e=>e}function ba(e,t){return Be(r=>r instanceof e,t)}function Oe(){return(e,t)=>new at(e.ast,Rh(t))}var Ue=Bh;function AV(e,t=void 0){return new Ri(e,t)}var rU=\"^\\\\S[\\\\s\\\\S]*\\\\S$|^\\\\S$|^$\";function JU(e){let t=new globalThis.RegExp(rU);return Ue(r=>r.trim()===r,{expected:\"a string with no leading or trailing whitespace\",representation:{id:\"effect/schema/isTrimmed\",payload:null},toJsonSchema:()=>({pattern:t.source}),toCode:()=>({runtime:\"Schema.isTrimmed()\"}),arbitraryConstraint:{patterns:[{source:rU,flags:\"\"}]},...e})}function zu(e,t){let r=e.source,n=e.flags,o=/^[dg]*uy?$/.test(n),s=n===\"\"?`new RegExp(${F(r)})`:`new RegExp(${F(r)}, ${F(n)})`;return ap(e,{toJsonSchema:()=>o?{pattern:n.endsWith(\"y\")?`^(?:${r})`:r}:[{},!0],toCode:()=>({runtime:`Schema.isPattern(${s})`}),...t})}function kV(e){return JI({toCode:()=>({runtime:\"Schema.isStringFinite()\"}),...e})}function vV(e){return KI({toCode:()=>({runtime:\"Schema.isStringBigInt()\"}),...e})}function OV(e){return VI({toCode:()=>({runtime:\"Schema.isStringSymbol()\"}),...e})}var _V=e=>e?new globalThis.RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${e}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`):/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$/;function RV(e,t){let r=_V(e);return zu(r,{expected:e?`a UUID v${e}`:\"a UUID\",representation:{id:\"effect/schema/isUUID\",payload:{version:e??null}},toJsonSchema:()=>({pattern:r.source,format:\"uuid\"}),toCode:()=>({runtime:e===void 0?\"Schema.isUUID()\":`Schema.isUUID(${e})`}),...t})}var nU=/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;function PV(e){return zu(nU,{expected:\"a GUID\",representation:{id:\"effect/schema/isGUID\",payload:null},toJsonSchema:()=>({pattern:nU.source}),toCode:()=>({runtime:\"Schema.isGUID()\"}),...e})}function MV(e){let t=/^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;return zu(t,{representation:{id:\"effect/schema/isULID\",payload:null},toJsonSchema:()=>({pattern:t.source}),toCode:()=>({runtime:\"Schema.isULID()\"}),...e})}function KU(e){let t=/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;return zu(t,{expected:\"a base64 encoded string\",representation:{id:\"effect/schema/isBase64\",payload:null},toJsonSchema:()=>({pattern:t.source}),toCode:()=>({runtime:\"Schema.isBase64()\"}),...e})}function FV(e){let t=/^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/;return zu(t,{expected:\"a base64url encoded string\",representation:{id:\"effect/schema/isBase64Url\",payload:null},toJsonSchema:()=>({pattern:t.source}),toCode:()=>({runtime:\"Schema.isBase64Url()\"}),...e})}function aw(e,t,r){if(e.length===0)return t.length===0?{}:[{},!0];let n={pattern:new globalThis.RegExp(r).source};return e===t?n:[n,!0]}function NV(e,t){let r=JSON.stringify(e),n=new globalThis.RegExp(`^${ks(e)}`),o=e.replace(/[\\uD800-\\uDBFF]$/,\"\");return Ue(s=>s.startsWith(e),{expected:`a string starting with ${r}`,representation:{id:\"effect/schema/isStartingWith\",payload:{startsWith:e}},toJsonSchema:()=>aw(o,e,`^${ks(o)}`),toCode:()=>({runtime:`Schema.isStartingWith(${F(e)})`}),arbitraryConstraint:{patterns:[{source:n.source,flags:n.flags}]},...t})}function UV(e,t){let r=JSON.stringify(e),n=new globalThis.RegExp(`${ks(e)}$`),o=e.replace(/^[\\uDC00-\\uDFFF]/,\"\");return Ue(s=>s.endsWith(e),{expected:`a string ending with ${r}`,representation:{id:\"effect/schema/isEndingWith\",payload:{endsWith:e}},toJsonSchema:()=>aw(o,e,`${ks(o)}$`),toCode:()=>({runtime:`Schema.isEndingWith(${F(e)})`}),arbitraryConstraint:{patterns:[{source:n.source,flags:n.flags}]},...t})}function DV(e,t){let r=JSON.stringify(e),n=new globalThis.RegExp(ks(e)),o=e.replace(/^[\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF]$/g,\"\");return Ue(s=>s.includes(e),{expected:`a string including ${r}`,representation:{id:\"effect/schema/isIncluding\",payload:{includes:e}},toJsonSchema:()=>aw(o,e,ks(o)),toCode:()=>({runtime:`Schema.isIncluding(${F(e)})`}),arbitraryConstraint:{patterns:[{source:n.source,flags:n.flags}]},...t})}var oU=\"^[^a-z]*$\";function LV(e){let t=new globalThis.RegExp(oU);return Ue(r=>r.toUpperCase()===r,{expected:\"a string with all characters in uppercase\",representation:{id:\"effect/schema/isUppercased\",payload:null},toJsonSchema:()=>[{pattern:t.source},!0],toCode:()=>({runtime:\"Schema.isUppercased()\"}),arbitraryConstraint:{patterns:[{source:oU,flags:\"\"}]},...e})}var sU=\"^[^A-Z]*$\";function $V(e){let t=new globalThis.RegExp(sU);return Ue(r=>r.toLowerCase()===r,{expected:\"a string with all characters in lowercase\",representation:{id:\"effect/schema/isLowercased\",payload:null},toJsonSchema:()=>[{pattern:t.source},!0],toCode:()=>({runtime:\"Schema.isLowercased()\"}),arbitraryConstraint:{patterns:[{source:sU,flags:\"\"}]},...e})}var aU=\"^(?:[^a-z][\\\\s\\\\S]*)?$\";function BV(e){let t=new globalThis.RegExp(aU);return Ue(r=>r.charAt(0).toUpperCase()===r.charAt(0),{expected:\"a string with the first character in uppercase\",representation:{id:\"effect/schema/isCapitalized\",payload:null},toJsonSchema:()=>[{pattern:t.source},!0],toCode:()=>({runtime:\"Schema.isCapitalized()\"}),arbitraryConstraint:{patterns:[{source:aU,flags:\"\"}]},...e})}var iU=\"^(?:[^A-Z][\\\\s\\\\S]*)?$\";function jV(e){let t=new globalThis.RegExp(iU);return Ue(r=>r.charAt(0).toLowerCase()===r.charAt(0),{expected:\"a string with the first character in lowercase\",representation:{id:\"effect/schema/isUncapitalized\",payload:null},toJsonSchema:()=>[{pattern:t.source},!0],toCode:()=>({runtime:\"Schema.isUncapitalized()\"}),arbitraryConstraint:{patterns:[{source:iU,flags:\"\"}]},...e})}var qu=q($I),zV=LI;function Op(e){let t=Tc(e.order),r=e.formatter??F;return(n,o)=>Ue(s=>t(s,n),{expected:`a value greater than ${r(n)}`,arbitraryConstraint:{order:e.order,minimum:n,exclusiveMinimum:!0},...e.annotate?.(n),...o})}function _p(e){let t=_d(e.order),r=e.formatter??F;return(n,o)=>Ue(s=>t(s,n),{expected:`a value greater than or equal to ${r(n)}`,arbitraryConstraint:{order:e.order,minimum:n},...e.annotate?.(n),...o})}function Rp(e){let t=vd(e.order),r=e.formatter??F;return(n,o)=>Ue(s=>t(s,n),{expected:`a value less than ${r(n)}`,arbitraryConstraint:{order:e.order,maximum:n,exclusiveMaximum:!0},...e.annotate?.(n),...o})}function Pp(e){let t=Od(e.order),r=e.formatter??F;return(n,o)=>Ue(s=>t(s,n),{expected:`a value less than or equal to ${r(n)}`,arbitraryConstraint:{order:e.order,maximum:n},...e.annotate?.(n),...o})}function Mp(e){let t=_d(e.order),r=Tc(e.order),n=Od(e.order),o=vd(e.order),s=e.formatter??F;return(a,i)=>{let c=a.exclusiveMinimum?r:t,u=a.exclusiveMaximum?o:n;return Ue(f=>c(f,a.minimum)&&u(f,a.maximum),{expected:`a value between ${s(a.minimum)}${a.exclusiveMinimum?\" (excluded)\":\"\"} and ${s(a.maximum)}${a.exclusiveMaximum?\" (excluded)\":\"\"}`,arbitraryConstraint:{order:e.order,minimum:a.minimum,maximum:a.maximum,...a.exclusiveMinimum&&{exclusiveMinimum:!0},...a.exclusiveMaximum&&{exclusiveMaximum:!0}},...e.annotate?.(a),...i})}}function qV(e){return(t,r)=>{let n=e.formatter??F;return Ue(o=>e.remainder(o,t)===e.zero,{expected:`a value that is a multiple of ${n(t)}`,...e.annotate?.(t),...r})}}function Nu(e){if(!globalThis.Number.isFinite(e))throw new globalThis.RangeError(`Expected a finite number, got ${F(e)}`);return e}var HV=Op({order:fs,annotate:e=>({representation:{id:\"effect/schema/isGreaterThan\",payload:{exclusiveMinimum:Nu(e)}},toJsonSchema:()=>({exclusiveMinimum:e}),toCode:()=>({runtime:`Schema.isGreaterThan(${F(e)})`})})}),GU=_p({order:fs,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanOrEqualTo\",payload:{minimum:Nu(e)}},toJsonSchema:()=>({minimum:e}),toCode:()=>({runtime:`Schema.isGreaterThanOrEqualTo(${F(e)})`})})}),WV=Rp({order:fs,annotate:e=>({representation:{id:\"effect/schema/isLessThan\",payload:{exclusiveMaximum:Nu(e)}},toJsonSchema:()=>({exclusiveMaximum:e}),toCode:()=>({runtime:`Schema.isLessThan(${F(e)})`})})}),JV=Pp({order:fs,annotate:e=>({representation:{id:\"effect/schema/isLessThanOrEqualTo\",payload:{maximum:Nu(e)}},toJsonSchema:()=>({maximum:e}),toCode:()=>({runtime:`Schema.isLessThanOrEqualTo(${F(e)})`})})}),Hu=Mp({order:fs,annotate:e=>{let t=e.exclusiveMinimum?!0:void 0,r=e.exclusiveMaximum?!0:void 0;return{representation:{id:\"effect/schema/isBetween\",payload:{minimum:Nu(e.minimum),maximum:Nu(e.maximum),...t&&{exclusiveMinimum:t},...r&&{exclusiveMaximum:r}}},toJsonSchema:()=>({[t?\"exclusiveMinimum\":\"minimum\"]:e.minimum,[r?\"exclusiveMaximum\":\"maximum\"]:e.maximum}),toCode:()=>({runtime:`Schema.isBetween({ minimum: ${F(e.minimum)}, maximum: ${F(e.maximum)}, exclusiveMinimum: ${F(t)}, exclusiveMaximum: ${F(r)} })`})}}});function KV(e,t){if(!globalThis.Number.isFinite(e)||e===0)throw new globalThis.RangeError(`Expected a finite non-zero number, got ${globalThis.String(e)}`);return e=globalThis.Math.abs(e),Ue(r=>I_(r,e)===0,{expected:`a value that is a multiple of ${e}`,representation:{id:\"effect/schema/isMultipleOf\",payload:{divisor:e}},toJsonSchema:()=>({multipleOf:e}),toCode:()=>({runtime:`Schema.isMultipleOf(${F(e)})`}),...t})}function vx(e){return Ue(t=>globalThis.Number.isSafeInteger(t),{expected:\"an integer\",representation:{id:\"effect/schema/isInt\",payload:null},toJsonSchema:()=>[{type:\"integer\"},!0],toCode:()=>({runtime:\"Schema.isInt()\"}),arbitraryConstraint:{number:\"integer\"},...e})}var lo=Wi.check(vx()),zi=lo.check(GU(0));function GV(e){return new Ri([vx(),Hu({minimum:-2147483648,maximum:2147483647})],{expected:\"a 32-bit integer\",...e})}function VV(e){return new Ri([vx(),Hu({minimum:0,maximum:4294967295})],{expected:\"a 32-bit unsigned integer\",...e})}function Uu(e){if(globalThis.Number.isNaN(e.getTime()))throw new globalThis.RangeError(`Expected a valid Date, got ${F(e)}`);return e.toISOString()}function Du(e){return`new Date(${F(e.getTime())})`}var ZV=Op({order:Ic,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanDate\",payload:{exclusiveMinimum:Uu(e)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isGreaterThanDate(${Du(e)})`})})}),YV=_p({order:Ic,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanOrEqualToDate\",payload:{minimum:Uu(e)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isGreaterThanOrEqualToDate(${Du(e)})`})})}),QV=Rp({order:Ic,annotate:e=>({representation:{id:\"effect/schema/isLessThanDate\",payload:{exclusiveMaximum:Uu(e)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isLessThanDate(${Du(e)})`})})}),XV=Pp({order:Ic,annotate:e=>({representation:{id:\"effect/schema/isLessThanOrEqualToDate\",payload:{maximum:Uu(e)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isLessThanOrEqualToDate(${Du(e)})`})})}),eZ=Mp({order:Ic,annotate:e=>{let t=e.exclusiveMinimum?!0:void 0,r=e.exclusiveMaximum?!0:void 0;return{representation:{id:\"effect/schema/isBetweenDate\",payload:{minimum:Uu(e.minimum),maximum:Uu(e.maximum),...t&&{exclusiveMinimum:t},...r&&{exclusiveMaximum:r}}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isBetweenDate({ minimum: ${Du(e.minimum)}, maximum: ${Du(e.maximum)}, exclusiveMinimum: ${F(t)}, exclusiveMaximum: ${F(r)} })`})}}}),tZ=Op({order:Ec,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanBigInt\",payload:{exclusiveMinimum:e.toString(10)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isGreaterThanBigInt(${F(e)})`})})}),VU=_p({order:Ec,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanOrEqualToBigInt\",payload:{minimum:e.toString(10)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isGreaterThanOrEqualToBigInt(${F(e)})`})})}),rZ=Rp({order:Ec,annotate:e=>({representation:{id:\"effect/schema/isLessThanBigInt\",payload:{exclusiveMaximum:e.toString(10)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isLessThanBigInt(${F(e)})`})})}),nZ=Pp({order:Ec,annotate:e=>({representation:{id:\"effect/schema/isLessThanOrEqualToBigInt\",payload:{maximum:e.toString(10)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isLessThanOrEqualToBigInt(${F(e)})`})})}),oZ=Mp({order:Ec,annotate:e=>{let t=e.exclusiveMinimum?!0:void 0,r=e.exclusiveMaximum?!0:void 0;return{representation:{id:\"effect/schema/isBetweenBigInt\",payload:{minimum:e.minimum.toString(10),maximum:e.maximum.toString(10),...t&&{exclusiveMinimum:t},...r&&{exclusiveMaximum:r}}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isBetweenBigInt({ minimum: ${F(e.minimum)}, maximum: ${F(e.maximum)}, exclusiveMinimum: ${F(t)}, exclusiveMaximum: ${F(r)} })`})}}});function ZU(e,t){return e=xr(e),YU(e,Math.ceil(e/2),t)}function YU(e,t,r){return Ue(n=>n.length>=e,{expected:`a value with a length of at least ${e}`,representation:{id:\"effect/schema/isMinLength\",payload:{minLength:e}},toJsonSchema:({type:n})=>n===\"string\"?e<=1?{minLength:t}:[{minLength:t},!0]:n===\"array\"?{minItems:e}:n===void 0?[{minLength:t,minItems:e},!0]:[{},!0],toCode:()=>({runtime:`Schema.isMinLength(${e})`}),[Lr]:!0,arbitraryConstraint:{minLength:e},...r})}function QU(e){return YU(1,1,e)}function XU(e,t){return e=xr(e),Ue(r=>r.length<=e,{expected:`a value with a length of at most ${e}`,representation:{id:\"effect/schema/isMaxLength\",payload:{maxLength:e}},toJsonSchema:({type:r})=>r===\"string\"?e===0?{maxLength:e}:[{maxLength:e},!0]:r===\"array\"?{maxItems:e}:r===void 0?[{maxLength:e,maxItems:e},!0]:[{},!0],toCode:()=>({runtime:`Schema.isMaxLength(${e})`}),[Lr]:!0,arbitraryConstraint:{maxLength:e},...t})}function iw(e,t,r){return e=xr(e),t=xr(t),Ue(n=>n.length>=e&&n.length<=t,{expected:e===t?`a value with a length of ${e}`:`a value with a length between ${e} and ${t}`,representation:{id:\"effect/schema/isBetweenLength\",payload:{minimum:e,maximum:t}},toJsonSchema:({type:n})=>n===\"string\"?t===0||Math.ceil(e/2)>t?{minLength:Math.ceil(e/2),maxLength:t}:[{minLength:Math.ceil(e/2),maxLength:t},!0]:n===\"array\"?{minItems:e,maxItems:t}:n===void 0?[{minLength:Math.ceil(e/2),maxLength:t,minItems:e,maxItems:t},!0]:[{},!0],toCode:()=>({runtime:`Schema.isBetweenLength(${e}, ${t})`}),[Lr]:!0,arbitraryConstraint:{minLength:e,maxLength:t},...r})}function sZ(e,t){return e=xr(e),Ue(r=>cw(r,e)>=e,{expected:`a string with at least ${e} code points`,representation:{id:\"effect/schema/isMinCodePoints\",payload:{minCodePoints:e}},toJsonSchema:()=>({minLength:e}),toCode:()=>({runtime:`Schema.isMinCodePoints(${e})`}),arbitraryConstraint:{minCodePoints:e},...t})}function aZ(e,t){return e=xr(e),Ue(r=>cw(r,e+1)<=e,{expected:`a string with at most ${e} code points`,representation:{id:\"effect/schema/isMaxCodePoints\",payload:{maxCodePoints:e}},toJsonSchema:()=>({maxLength:e}),toCode:()=>({runtime:`Schema.isMaxCodePoints(${e})`}),arbitraryConstraint:{maxCodePoints:e},...t})}function iZ(e,t,r){return e=xr(e),t=xr(t),Ue(n=>{let o=cw(n,t+1);return o>=e&&o<=t},{expected:e===t?`a string with ${e} code points`:`a string with between ${e} and ${t} code points`,representation:{id:\"effect/schema/isBetweenCodePoints\",payload:{minimum:e,maximum:t}},toJsonSchema:()=>({allOf:[{minLength:e},{maxLength:t}]}),toCode:()=>({runtime:`Schema.isBetweenCodePoints(${e}, ${t})`}),arbitraryConstraint:{minCodePoints:e,maxCodePoints:t},...r})}function cw(e,t){if(t===0)return 0;let r=0;for(let n of e)if(++r>=t)break;return r}function xr(e){if(!globalThis.Number.isFinite(e))throw new globalThis.RangeError(`Expected a finite number, got ${e}`);return Math.max(0,Math.floor(e))}function cZ(e,t){return e=xr(e),Ue(r=>r.size>=e,{expected:`a value with a size of at least ${e}`,representation:{id:\"effect/schema/isMinSize\",payload:{minSize:e}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isMinSize(${e})`}),[Lr]:!0,arbitraryConstraint:{minSize:e},...t})}function uZ(e,t){return e=xr(e),Ue(r=>r.size<=e,{expected:`a value with a size of at most ${e}`,representation:{id:\"effect/schema/isMaxSize\",payload:{maxSize:e}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isMaxSize(${e})`}),[Lr]:!0,arbitraryConstraint:{maxSize:e},...t})}function fZ(e,t,r){return e=xr(e),t=xr(t),Ue(n=>n.size>=e&&n.size<=t,{expected:e===t?`a value with a size of ${e}`:`a value with a size between ${e} and ${t}`,representation:{id:\"effect/schema/isBetweenSize\",payload:{minimum:e,maximum:t}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isBetweenSize(${e}, ${t})`}),[Lr]:!0,arbitraryConstraint:{minSize:e,maxSize:t},...r})}function lZ(e,t){return e=xr(e),Ue(r=>Reflect.ownKeys(r).length>=e,{expected:`a value with at least ${e===1?\"1 entry\":`${e} entries`}`,representation:{id:\"effect/schema/isMinProperties\",payload:{minProperties:e}},toJsonSchema:({type:r})=>r===\"object\"?{minProperties:e}:[{minProperties:e},!0],toCode:()=>({runtime:`Schema.isMinProperties(${e})`}),[Lr]:!0,arbitraryConstraint:{minProperties:e},...t})}function pZ(e,t){return e=xr(e),Ue(r=>Reflect.ownKeys(r).length<=e,{expected:`a value with at most ${e===1?\"1 entry\":`${e} entries`}`,representation:{id:\"effect/schema/isMaxProperties\",payload:{maxProperties:e}},toJsonSchema:({type:r})=>r===\"object\"?{maxProperties:e}:[{maxProperties:e},!0],toCode:()=>({runtime:`Schema.isMaxProperties(${e})`}),[Lr]:!0,arbitraryConstraint:{maxProperties:e},...t})}function dZ(e,t,r){return e=xr(e),t=xr(t),Ue(n=>Reflect.ownKeys(n).length>=e&&Reflect.ownKeys(n).length<=t,{expected:e===t?`a value with exactly ${e===1?\"1 entry\":`${e} entries`}`:`a value with between ${e} and ${t} entries`,representation:{id:\"effect/schema/isBetweenProperties\",payload:{minimum:e,maximum:t}},toJsonSchema:({type:n})=>n===\"object\"?{minProperties:e,maxProperties:t}:[{minProperties:e,maxProperties:t},!0],toCode:()=>({runtime:`Schema.isBetweenProperties(${e}, ${t})`}),[Lr]:!0,arbitraryConstraint:{minProperties:e,maxProperties:t},...r})}function mZ(e,t){let r=$u(e),n=vF(r.ast);return Ue((o,s,a)=>{let i=Reflect.ownKeys(o),c=[];for(let u of i){let f=n(u,a);if(f!==void 0&&(c.push(new ut([u],f)),a.errors===\"first\"))break}return or(c)?new wt(s,c,o,a):!0},{expected:\"an object with property names matching the schema\",representation:{id:\"effect/schema/isPropertyNames\",payload:null,schemas:[r.ast]},toJsonSchema:({schemas:o,type:s})=>s===\"object\"?{propertyNames:o[0]}:[{propertyNames:o[0]},!0],toCode:({schemas:o})=>({runtime:`Schema.isPropertyNames(${o[0].runtime})`}),[Lr]:!0,...t})}function uw(e){return Ue(t=>zd(t).length===t.length,{expected:\"an array with unique items\",representation:{id:\"effect/schema/isUnique\",payload:null},toJsonSchema:()=>({uniqueItems:!0}),toCode:()=>({runtime:\"Schema.isUnique()\"}),arbitraryConstraint:{uniqueBy:R},...e})}function eD(e){return Ue(t=>zd(t.map(([r])=>r)).length===t.length,{expected:\"an array with unique keys\",representation:{id:\"effect/schema/isUniqueKey\",payload:null},toCode:()=>({runtime:\"Schema.isUniqueKey()\"}),arbitraryConstraint:{uniqueBy:t=>t[0]},...e})}var fw=K.check(QU()),hZ=K.check(iw(1,1)),tD=e=>(e?.includeStack===!0?1:0)|(e?.excludeCause===!0?2:0),rD=e=>{switch(e){case 0:return;case 1:return{includeStack:!0};case 2:return{excludeCause:!0};case 3:return{includeStack:!0,excludeCause:!0}}},cU=[];function xZ(e){let t=tD(e),r=cU[t];if(r!==void 0)return r;let n=rD(t),o=ba(globalThis.Error,{representation:{id:\"effect/schema/Error\",payload:n??null},toCode:()=>({runtime:n!==void 0?`Schema.ErrorInstance(${F(n)})`:\"Schema.ErrorInstance()\",Type:\"globalThis.Error\"}),expected:\"Error\",toCodecJson:()=>Oe()($8,lM(n))});return cU[t]=o,o}var uU=[];function gZ(e){let t=tD(e),r=uU[t];if(r!==void 0)return r;let n=Nx.pipe(G(vp,pM(rD(t))));return uU[t]=n,n}function yZ(e,t){let r=qr()([e,t],([n,o])=>{let s=$e(Qo([n,o]));return(a,i,c)=>a instanceof globalThis.Map?Vt(nt(s)([...a],c),{onSuccess:u=>new globalThis.Map(u),onFailure:u=>Br(i,\"entries\",u,a,c)}):P(new ft(i,a,c))},{representation:{id:\"effect/schema/ReadonlyMap\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.ReadonlyMap(${n[0].runtime}, ${n[1].runtime})`,Type:`globalThis.ReadonlyMap<${n[0].Type}, ${n[1].Type}>`}),expected:\"ReadonlyMap\",toCodec:([n,o])=>Oe()($e(Qo([n,o])),Pe({decode:s=>new globalThis.Map(s),encode:s=>[...s.entries()]}))});return q(r.ast,{key:e,value:t})}function SZ(e){let t=qr()([e],([r])=>{let n=$e(r);return(o,s,a)=>o instanceof globalThis.Set?Vt(nt(n)([...o],a),{onSuccess:i=>new globalThis.Set(i),onFailure:i=>Br(s,\"values\",i,o,a)}):P(new ft(s,o,a))},{representation:{id:\"effect/schema/ReadonlySet\",payload:null},toCode:({typeParameters:r})=>({runtime:`Schema.ReadonlySet(${r[0].runtime})`,Type:`globalThis.ReadonlySet<${r[0].Type}>`}),expected:\"ReadonlySet\",toCodec:([r])=>Oe()($e(r),Pe({decode:n=>new globalThis.Set(n),encode:n=>[...n.values()]}))});return q(t.ast,{value:e})}var bZ=ba(globalThis.RegExp,{representation:{id:\"effect/schema/RegExp\",payload:null},toCode:()=>({runtime:\"Schema.RegExp\",Type:\"globalThis.RegExp\"}),expected:\"RegExp\",toCodecJson:()=>Oe()(Me({source:K,flags:K}),Yt({decode:(e,t)=>Qc({try:()=>new globalThis.RegExp(e.source,e.flags),catch:()=>new de({expected:\"valid RegExp source and flags\"},e,t)}),encode:e=>g({source:e.source,flags:e.flags})}))}),nD=K.annotate({expected:\"a string that will be decoded as a URL\"}),Ox=ba(globalThis.URL,{representation:{id:\"effect/schema/URL\",payload:null},toCode:()=>({runtime:\"Schema.URL\",Type:\"globalThis.URL\"}),expected:\"URL\",toCodecJson:()=>Oe()(nD,hI)}),EZ=nD.pipe(G(Ox,hI)),oD=K.annotate({expected:\"a string that will be decoded as a Date\"}),Wu=Be(e=>e instanceof globalThis.Date&&!globalThis.Number.isNaN(e.getTime()),{representation:{id:\"effect/schema/Date\",payload:null},toCode:()=>({runtime:\"Schema.Date\",Type:\"globalThis.Date\"}),expected:\"a valid Date\",toCodecJson:()=>Oe()(oD,dI)}),IZ=oD.pipe(G(Wu,dI)),TZ=lo.pipe(G(Wu,sM)),wZ=K.annotate({expected:\"a string that will be decoded as JSON\",contentMediaType:\"application/json\"});function lw(e,t){return wZ.pipe(G(e,IM(t)))}var CZ=lw(vp),sD=ba(globalThis.File,{representation:{id:\"effect/schema/File\",payload:null},toCode:()=>({runtime:\"Schema.File\",Type:\"globalThis.File\"}),expected:\"File\",toCodecJson:()=>Oe()(Me({data:K.check(KU()),type:K,name:K,lastModified:lo}),Yt({decode:(e,t)=>wr(Ei(e.data),{onFailure:()=>P(new de({expected:\"a valid Base64 string\"},e.data,t)),onSuccess:r=>{let n=new globalThis.Uint8Array(r);return g(new globalThis.File([n],e.name,{type:e.type,lastModified:e.lastModified}))}}),encode:(e,t)=>Zc({try:async()=>{let r=new globalThis.Uint8Array(await e.arrayBuffer());return{data:su(r),type:e.type,name:e.name,lastModified:e.lastModified}},catch:()=>new de({expected:\"a readable File\"},e,t)})}))}),aD=ba(globalThis.FormData,{representation:{id:\"effect/schema/FormData\",payload:null},toCode:()=>({runtime:\"Schema.FormData\",Type:\"globalThis.FormData\"}),expected:\"FormData\",toCodecJson:()=>Oe()($e(Qo([K,it([Me({_tag:Hi(\"String\"),value:K}),Me({_tag:Hi(\"File\"),value:sD})])])),Yt({decode:e=>{let t=new globalThis.FormData;for(let[r,n]of e)t.append(r,n.value);return g(t)},encode:e=>g(globalThis.Array.from(e.entries()).map(([t,r])=>typeof r==\"string\"?[t,{_tag:\"String\",value:r}]:[t,{_tag:\"File\",value:r}]))}))});function AZ(e){return aD.pipe(G(e,TM))}var iD=ba(globalThis.URLSearchParams,{representation:{id:\"effect/schema/URLSearchParams\",payload:null},toCode:()=>({runtime:\"Schema.URLSearchParams\",Type:\"globalThis.URLSearchParams\"}),expected:\"URLSearchParams\",toCodecJson:()=>Oe()(K.annotate({expected:\"a query string that will be decoded as URLSearchParams\"}),Pe({decode:e=>new globalThis.URLSearchParams(e),encode:e=>e.toString()}))});function kZ(e){return iD.pipe(G(e,wM))}var vZ=K.annotate({expected:\"a string that will be decoded as a number\"}).pipe(G(Wi,pu)),OZ=K.annotate({expected:\"a string that will be decoded as a finite number\"}).pipe(G(qu,pu)),_Z=q(GI).pipe(G(Bu,Mh)),cD=K.check(JU()),RZ=K.annotate({expected:\"a string that will be decoded as a trimmed string\"}).pipe(G(cD,nM())),PZ=K.annotate({expected:\"a base64 encoded string that will be decoded as a UTF-8 string\"}).pipe(G(K,yM)),MZ=K.annotate({expected:\"a base64 (URL) encoded string that will be decoded as a UTF-8 string\"}).pipe(G(K,SM)),FZ=K.annotate({expected:\"a hex encoded string that will be decoded as a UTF-8 string\"}).pipe(G(K,bM)),NZ=K.annotate({expected:\"a URI component encoded string that will be decoded as a UTF-8 string\"}).pipe(G(K,EM)),KT=it([qu,OU,K]),UZ=Me({issues:$e(Me({message:K,path:Lu($e(it([KT,Me({key:KT})])))}))}),DZ=fo([0,1]).pipe(G(XT,Pe({decode:e=>e===1,encode:e=>e?1:0}))),uD=K.annotate({expected:\"a base64 encoded string that will be decoded as Uint8Array\",format:\"byte\",contentEncoding:\"base64\"}),_x=ba(globalThis.Uint8Array,{representation:{id:\"effect/schema/Uint8Array\",payload:null},toCode:()=>({runtime:\"Schema.Uint8Array\",Type:\"globalThis.Uint8Array\"}),expected:\"Uint8Array\",toCodecJson:()=>Oe()(uD,xI)}),LZ=uD.pipe(G(_x,xI)),$Z=K.annotate({expected:\"a base64 (URL) encoded string that will be decoded as a Uint8Array\"}).pipe(G(_x,{decode:WP(),encode:vh()})),BZ=K.annotate({expected:\"a hex encoded string that will be decoded as a Uint8Array\"}).pipe(G(_x,{decode:KP(),encode:Oh()})),fD=Yt({decode:(e,t)=>{let r=iR(e);return fe(r)?P(new de({expected:\"a valid BigDecimal string\"},e,t)):g(r.value)},encode:e=>g(ws(e))}),jZ=Op({order:nu,formatter:e=>ws(e)}),zZ=_p({order:nu,formatter:e=>ws(e)}),qZ=Rp({order:nu,formatter:e=>ws(e)}),HZ=Pp({order:nu,formatter:e=>ws(e)}),WZ=Mp({order:nu,formatter:e=>ws(e)}),lD=K.annotate({expected:\"a string that will be decoded as a BigDecimal\"}),pD=Be(iE,{representation:{id:\"effect/schema/BigDecimal\",payload:null},toCode:()=>({runtime:\"Schema.BigDecimal\",Type:\"BigDecimal.BigDecimal\",importDeclarations:['import * as BigDecimal from \"effect/BigDecimal\"']}),expected:\"BigDecimal\",toCodecJson:()=>Oe()(lD,fD),toFormatter:()=>e=>ws(e)}),JZ=lD.pipe(G(pD,fD)),Ju=Be(gR,{representation:{id:\"effect/schema/ByteSize\",payload:null},toCode:()=>({runtime:\"Schema.ByteSize\",Type:\"ByteSize.ByteSize\",importDeclarations:['import * as ByteSize from \"effect/ByteSize\"']}),expected:\"ByteSize\",toCodecJson:()=>Oe()(Bu.check(VU(globalThis.BigInt(0))),Pe({decode:xR,encode:uh})),toCodecStringTree:()=>Oe()(dD,mI)}),dD=K.annotate({expected:\"a string that will be decoded as a ByteSize\"}),KZ=dD.pipe(G(Ju,mI)),GZ=Bu.pipe(G(Ju,aM)),VZ=Wi.pipe(G(Ju,iM));function GT(e,t){let r=qr()([e,t],([n,o])=>(s,a,i)=>{if(!tb(s))return P(new ft(a,s,i));switch(s._tag){case\"Fail\":return Vt(nt(n)(s.error,i),{onSuccess:jc,onFailure:c=>Br(a,\"error\",c,s,i)});case\"Die\":return Vt(nt(o)(s.defect,i),{onSuccess:zc,onFailure:c=>Br(a,\"defect\",c,s,i)});case\"Interrupt\":return g(s)}},{representation:{id:\"effect/schema/CauseReason\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.CauseReason(${n[0].runtime}, ${n[1].runtime})`,Type:`Cause.Failure<${n[0].Type}, ${n[1].Type}>`,importDeclarations:['import * as Cause from \"effect/Cause\"']}),expected:\"Cause.Failure\",toCodec:([n,o])=>Oe()(it([Me({_tag:Ct(\"Fail\"),error:n}),Me({_tag:Ct(\"Die\"),defect:o}),Me({_tag:Ct(\"Interrupt\"),fiberId:Ax(qu)})]),Pe({decode:s=>{switch(s._tag){case\"Fail\":return jc(s.error);case\"Die\":return zc(s.defect);case\"Interrupt\":return nb(s.fiberId)}},encode:s=>{switch(s._tag){case\"Fail\":return{_tag:\"Fail\",error:s.error};case\"Die\":return{_tag:\"Die\",defect:s.defect};case\"Interrupt\":return{_tag:\"Interrupt\",fiberId:s.fiberId}}}}))});return q(r.ast,{error:e,defect:t})}function VT(e,t){let r=qr()([e,t],([n,o])=>{let s=$e(GT(n,o));return(a,i,c)=>eb(a)?Vt(nt(s)(a.reasons,c),{onSuccess:oa,onFailure:u=>Br(i,\"failures\",u,a,c)}):P(new ft(i,a,c))},{representation:{id:\"effect/schema/Cause\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.Cause(${n[0].runtime}, ${n[1].runtime})`,Type:`Cause.Cause<${n[0].Type}, ${n[1].Type}>`,importDeclarations:['import * as Cause from \"effect/Cause\"']}),expected:\"Cause\",toCodec:([n,o])=>Oe()($e(GT(n,o)),Pe({decode:oa,encode:({reasons:s})=>s}))});return q(r.ast,{error:e,defect:t})}function fU(e){return Oe()(e,Pe({decode:hE,encode:Ye}))}function ZZ(e){let t=qr()([e],([r])=>{let n=$e(r);return(o,s,a)=>lh(o)?Vt(nt(n)(Ye(o),a),{onSuccess:hE,onFailure:i=>Br(s,\"values\",i,o,a)}):P(new ft(s,o,a))},{representation:{id:\"effect/schema/Chunk\",payload:null},toCode:({typeParameters:r})=>({runtime:`Schema.Chunk(${r[0].runtime})`,Type:`Chunk.Chunk<${r[0].Type}>`}),expected:\"Chunk\",toCodec:([r])=>fU($e(r)),toCodecArbitrary:({constraint:r,typeParameters:[n]})=>fU(Cx($e(n),r?.minLength,r?.maxLength)),toEquivalence:([r])=>dE(r)});return q(t.ast,{value:e})}function mD(){return gt((e,t)=>Wt(yE(e),{onNone:()=>P(new de({message:\"Invalid DateTime input\"},e,t)),onSome:r=>g(SE(r))}))}var YZ=Pe({decode:e=>EE(e),encode:e=>e.offset}),hD=Yt({decode:(e,t)=>Wt(PR(e),{onNone:()=>P(new de({expected:\"a valid IANA time zone\"},e,t)),onSome:g}),encode:e=>g(e.id)}),xD=Yt({decode:(e,t)=>Wt(MR(e),{onNone:()=>P(new de({expected:\"a valid time zone\"},e,t)),onSome:g}),encode:e=>g(Dl(e))}),gD=Yt({decode:(e,t)=>Wt(yE(e),{onNone:()=>P(new de({expected:\"a valid UTC DateTime string\"},e,t)),onSome:r=>g(SE(r))}),encode:e=>g(UR(e))}),yD=Yt({decode:(e,t)=>Wt(RR(e),{onNone:()=>P(new de({expected:\"a valid Zoned DateTime string\"},e,t)),onSome:g}),encode:e=>g(IE(e))}),SD=-864e13,bD=864e13,QZ=SD+840*60*1e3,XZ=bD-840*60*1e3,e5=-720*60*1e3,t5=840*60*1e3,ED=[\"UTC\",\"Europe/London\",\"America/New_York\",\"Asia/Tokyo\",\"Australia/Sydney\"];function ID(e,t,r){let n=Math.max(t,e?.minimum===void 0?t:e.minimum.epochMilliseconds+(e.exclusiveMinimum===!0?1:0)),o=Math.min(r,e?.maximum===void 0?r:e.maximum.epochMilliseconds-(e.exclusiveMaximum===!0?1:0));return n<=o?[n,o]:[t,r]}function pw(e,t){return lo.check(Hu({minimum:e,maximum:t}))}function TD(){return it([pw(e5,t5),fo(ED)])}var Rx=Be(e=>gE(e)&&AR(e),{representation:{id:\"effect/schema/DateTimeUtc\",payload:null},toCode:()=>({runtime:\"Schema.DateTimeUtc\",Type:\"DateTime.Utc\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.Utc\",toCodecArbitrary:({constraint:e})=>{let[t,r]=ID(e,SD,bD);return ju()(pw(t,r),oe(OR))},toCodecJson:()=>Oe()(K,gD),toFormatter:()=>e=>e.toString()}),r5=Wu.pipe(G(Rx,{decode:mD(),encode:oe(FR)})),n5=K.annotate({expected:\"a string that will be decoded as a DateTime.Utc\"}).pipe(G(Rx,gD)),o5=lo.pipe(G(Rx,{decode:mD(),encode:oe(NR)})),s5=Be(wR,{representation:{id:\"effect/schema/TimeZoneOffset\",payload:null},toCode:()=>({runtime:\"Schema.TimeZoneOffset\",Type:\"DateTime.TimeZone.Offset\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.TimeZone.Offset\",toCodecJson:()=>Oe()(lo,YZ),toFormatter:()=>e=>Dl(e)}),wD=K.annotate({expected:\"an IANA time zone identifier\"}),CD=Be(CR,{representation:{id:\"effect/schema/TimeZoneNamed\",payload:null},toCode:()=>({runtime:\"Schema.TimeZoneNamed\",Type:\"DateTime.TimeZone.Named\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.TimeZone.Named\",toCodecArbitrary:()=>ju()(fo(ED),oe(bE)),toCodecJson:()=>Oe()(wD,hD),toFormatter:()=>e=>Dl(e)}),a5=wD.pipe(G(CD,hD)),AD=K.annotate({expected:\"a time zone string (IANA identifier or offset like +03:00)\"}),kD=Be(TR,{representation:{id:\"effect/schema/TimeZone\",payload:null},toCode:()=>({runtime:\"Schema.TimeZone\",Type:\"DateTime.TimeZone\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.TimeZone\",toCodecArbitrary:()=>ju()(TD(),oe(e=>typeof e==\"number\"?EE(e):bE(e))),toCodecJson:()=>Oe()(AD,xD),toFormatter:()=>e=>Dl(e)}),i5=AD.pipe(G(kD,xD)),vD=K.annotate({expected:\"a zoned DateTime string (e.g. 2024-01-01T00:00:00.000+00:00[Europe/London])\"}),OD=Be(e=>gE(e)&&kR(e),{representation:{id:\"effect/schema/DateTimeZoned\",payload:null},toCode:()=>({runtime:\"Schema.DateTimeZoned\",Type:\"DateTime.Zoned\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.Zoned\",toCodecArbitrary:({constraint:e})=>{let[t,r]=ID(e,QZ,XZ);return ju()(Me({epochMilliseconds:pw(t,r),timeZone:TD()}),oe(({epochMilliseconds:n,timeZone:o})=>_R(n,{timeZone:o})))},toCodecJson:()=>Oe()(vD,yD),toFormatter:()=>e=>IE(e),toEquivalence:()=>vR}),c5=vD.pipe(G(OD,yD)),po=(e,t,r,n)=>K.pipe(G(e,Yt({decode:(o,s)=>{let a=t(o);return mt(a)?g(a.success):P(new de({message:a.failure.message},o,s))},encode:o=>g(r(o))}))).annotate({identifier:n}),Ji=Be(Pu,{identifier:\"MacAddress\"}),Ku=po(Ji,qN,PT,\"MacAddressFromString\"),Px=Be(hr,{identifier:\"Ipv4Address\"}),dw=po(Px,Mu,uo,\"Ipv4AddressFromString\"),mw=Be(Yo,{identifier:\"Ipv6Address\"}),_D=po(mw,Sp,uo,\"Ipv6AddressFromString\"),Ki=Be(jN,{identifier:\"IpAddress\"}),Gu=po(Ki,bp,uo,\"IpAddressFromString\"),Xe=(e,t,r,n)=>FU(t,{expected:r})(e).annotate({identifier:n}),u5=Xe(Ki,Fu,\"a multicast address\",\"IpMulticastAddress\"),f5=Xe(Gu,Fu,\"a multicast address\",\"IpMulticastAddressFromString\"),l5=Xe(Ji,Fu,\"a multicast address\",\"MacMulticastAddress\"),p5=Xe(Ku,Fu,\"a multicast address\",\"MacMulticastAddressFromString\"),d5=Xe(Ki,UT,\"a unicast address\",\"IpUnicastAddress\"),m5=Xe(Gu,UT,\"a unicast address\",\"IpUnicastAddressFromString\"),h5=Xe(Ji,mx,\"a unicast address\",\"MacUnicastAddress\"),x5=Xe(Ku,mx,\"a unicast address\",\"MacUnicastAddressFromString\"),g5=Xe(Px,xx,\"a broadcast address\",\"Ipv4BroadcastAddress\"),y5=Xe(dw,xx,\"a broadcast address\",\"Ipv4BroadcastAddressFromString\"),S5=Xe(Ji,dx,\"a broadcast address\",\"MacBroadcastAddress\"),b5=Xe(Ku,dx,\"a broadcast address\",\"MacBroadcastAddressFromString\"),E5=Xe(Ki,NT,\"a loopback address\",\"IpLoopbackAddress\"),I5=Xe(Gu,NT,\"a loopback address\",\"IpLoopbackAddressFromString\"),T5=Xe(Ki,DT,\"a link-local address\",\"IpLinkLocalAddress\"),w5=Xe(Gu,DT,\"a link-local address\",\"IpLinkLocalAddressFromString\"),C5=Xe(Ki,hx,\"an unspecified address\",\"IpUnspecifiedAddress\"),A5=Xe(Gu,hx,\"an unspecified address\",\"IpUnspecifiedAddressFromString\"),k5=Xe(Px,LT,\"a private address\",\"Ipv4PrivateAddress\"),v5=Xe(dw,LT,\"a private address\",\"Ipv4PrivateAddressFromString\"),O5=Xe(mw,$T,\"a unique-local address\",\"Ipv6UniqueLocalAddress\"),_5=Xe(_D,$T,\"a unique-local address\",\"Ipv6UniqueLocalAddressFromString\"),R5=Xe(Ji,MT,\"a locally administered address\",\"MacLocallyAdministeredAddress\"),P5=Xe(Ku,MT,\"a locally administered address\",\"MacLocallyAdministeredAddressFromString\"),M5=Xe(Ji,FT,\"a universally administered address\",\"MacUniversallyAdministeredAddress\"),F5=Xe(Ku,FT,\"a universally administered address\",\"MacUniversallyAdministeredAddressFromString\"),RD=Be(GN,{identifier:\"Ipv4Interface\"}),N5=po(RD,yx,Tp,\"Ipv4InterfaceFromString\"),PD=Be(VN,{identifier:\"Ipv6Interface\"}),U5=po(PD,Sx,Tp,\"Ipv6InterfaceFromString\"),MD=Be(Ep,{identifier:\"IpInterface\"}),D5=po(MD,bx,Tp,\"IpInterfaceFromString\"),FD=Be(YN,{identifier:\"Ipv4Network\"}),L5=po(FD,XN,Cp,\"Ipv4NetworkFromString\"),ND=Be(QN,{identifier:\"Ipv6Network\"}),$5=po(ND,eU,Cp,\"Ipv6NetworkFromString\"),UD=Be(wp,{identifier:\"IpNetwork\"}),B5=po(UD,tU,Cp,\"IpNetworkFromString\"),j5=Be(gp,{identifier:\"InetAddressV4\"}),z5=Be(yp,{identifier:\"InetAddressV6\"}),DD=Be(_T,{identifier:\"InetAddress\"}),q5=po(DD,JN,gx,\"InetAddressFromString\"),LD=Be(px,{identifier:\"UnixPathAddress\"}),H5=K.pipe(G(LD,Pe({decode:KN,encode:e=>e.path}))).annotate({identifier:\"UnixPathAddressFromString\"}),W5=Be(zN,{identifier:\"SocketAddress\"}),J5=Yt({decode:(e,t)=>Wt(kA(e),{onNone:()=>P(new de({expected:\"a valid Duration string\"},e,t)),onSome:g}),encode:e=>g(globalThis.String(e))}),K5=Yt({decode:e=>g(Ro(e)),encode:(e,t)=>Wt(OA(e),{onNone:()=>P(new de({expected:\"a Duration representable as a bigint\"},e,t)),onSome:r=>g(r)})}),G5=Pe({decode:e=>Js(e),encode:e=>Cr(e)}),Mx=Be(Ey,{representation:{id:\"effect/schema/Duration\",payload:null},toCode:()=>({runtime:\"Schema.Duration\",Type:\"Duration.Duration\",importDeclarations:['import * as Duration from \"effect/Duration\"']}),expected:\"Duration\",toCodecJson:()=>Oe()(it([Me({_tag:Ct(\"Infinity\")}),Me({_tag:Ct(\"NegativeInfinity\")}),Me({_tag:Ct(\"Nanos\"),value:Bu}),Me({_tag:Ct(\"Millis\"),value:lo})]),Pe({decode:e=>{switch(e._tag){case\"Infinity\":return ls;case\"NegativeInfinity\":return Ac;case\"Nanos\":return Ro(e.value);case\"Millis\":return Js(e.value)}},encode:e=>{switch(e.value._tag){case\"Infinity\":return{_tag:\"Infinity\"};case\"NegativeInfinity\":return{_tag:\"NegativeInfinity\"};case\"Nanos\":return{_tag:\"Nanos\",value:e.value.nanos};case\"Millis\":return{_tag:\"Millis\",value:e.value.millis}}}}))}),V5=K.annotate({expected:\"a string that will be decoded as a Duration\"}),hw=V5.pipe(G(Mx,J5)),Z5=Bu.pipe(G(Mx,K5)),Y5=Wi.pipe(G(Mx,G5));function Q5(e,t,r){let n=qr()([e,t,r],([o,s,a])=>{let i=VT(s,a);return(c,u,f)=>{if(!hl(c))return P(new ft(u,c,f));switch(c._tag){case\"Success\":return Vt(nt(o)(c.value,f),{onSuccess:vr,onFailure:p=>Br(u,\"value\",p,c,f)});case\"Failure\":return Vt(nt(i)(c.cause,f),{onSuccess:sn,onFailure:p=>Br(u,\"cause\",p,c,f)})}}},{representation:{id:\"effect/schema/Exit\",payload:null},toCode:({typeParameters:o})=>({runtime:`Schema.Exit(${o[0].runtime}, ${o[1].runtime}, ${o[2].runtime})`,Type:`Exit.Exit<${o[0].Type}, ${o[1].Type}, ${o[2].Type}>`,importDeclarations:['import * as Exit from \"effect/Exit\"']}),expected:\"Exit\",toCodec:([o,s,a])=>Oe()(it([Me({_tag:Ct(\"Success\"),value:o}),Me({_tag:Ct(\"Failure\"),cause:VT(s,a)})]),Pe({decode:i=>i._tag===\"Success\"?vr(i.value):sn(i.cause),encode:i=>Ve(i)?{_tag:\"Success\",value:i.value}:{_tag:\"Failure\",cause:i.cause}}))});return q(n.ast,{value:e,error:t,defect:r})}function lU(e,t,r){return Me({type:Ct(e),nodes:$e(Me({index:zi,data:t})),edges:$e(Me({index:zi,source:zi,target:zi,data:r}))})}function pU(e,t){let r=-1,n=new Set;for(let o=0;o{let o=Hl(r),s=Hl(n);if(o.type!==s.type||o.nodes.length!==s.nodes.length||o.edges.length!==s.edges.length)return!1;for(let a=0;a{if(n===null)return bh({type:e,nodes:[],edges:[]});let o=n.nodes.map((s,a)=>({index:a,data:s}));return bh({type:e,nodes:o,edges:n.edges.map(([s,a,i],c)=>({index:c,source:s%o.length,target:a%o.length,data:i}))})}))}function r8(e,t,r){let n=qr()([t,r],([o,s])=>{let a=lU(e,o,s);return(i,c,u)=>!QE(i)||i.mutable||i.type!==e?P(new ft(c,i,u)):I(nt(a)(Hl(i),u),f=>pU(f,u))},{representation:{id:\"effect/schema/Graph\",payload:e},toCode:({typeParameters:o})=>({runtime:`Schema.Graph(${F(e)}, ${o[0].runtime}, ${o[1].runtime})`,Type:`Graph.Graph<${o[0].Type}, ${o[1].Type}, ${F(e)}>`,importDeclarations:['import * as Graph from \"effect/Graph\"']}),expected:`an immutable ${e} Graph`,toCodec:([o,s])=>Oe()(lU(e,o,s),Yt({decode:pU,encode:(a,i)=>X5(a,e,i)})),toCodecArbitrary:({typeParameters:[o,s]})=>t8(e,o,s),toEquivalence:([o,s])=>e8(o,s)});return q(n.ast,{type:e,node:t,edge:r})}function dU(e){return Oe()(e,Pe({decode:PE,encode:zl}))}function n8(e,t){let r=qr()([e,t],([n,o])=>{let s=$e(Qo([n,o]));return(a,i,c)=>GR(a)?Vt(nt(s)(zl(a),c),{onSuccess:PE,onFailure:u=>Br(i,\"entries\",u,a,c)}):P(new ft(i,a,c))},{representation:{id:\"effect/schema/HashMap\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.HashMap(${n[0].runtime}, ${n[1].runtime})`,Type:`HashMap.HashMap<${n[0].Type}, ${n[1].Type}>`,importDeclarations:['import * as HashMap from \"effect/HashMap\"']}),expected:\"HashMap\",toCodec:([n,o])=>dU($e(Qo([n,o]))),toCodecArbitrary:({constraint:n,typeParameters:[o,s]})=>dU(Cx($e(Qo([o,s])).check(eD()),n?.minSize,n?.maxSize)),toEquivalence:([n,o])=>ld(n,o)});return q(r.ast,{key:e,value:t})}function mU(e){return Oe()(e,Pe({decode:NE,encode:Ye}))}function o8(e){let t=qr()([e],([r])=>{let n=$e(r);return(o,s,a)=>XR(o)?Vt(nt(n)(Ye(o),a),{onSuccess:NE,onFailure:i=>Br(s,\"values\",i,o,a)}):P(new ft(s,o,a))},{representation:{id:\"effect/schema/HashSet\",payload:null},toCode:({typeParameters:r})=>({runtime:`Schema.HashSet(${r[0].runtime})`,Type:`HashSet.HashSet<${r[0].Type}>`}),expected:\"HashSet\",toCodec:([r])=>mU($e(r)),toCodecArbitrary:({constraint:r,typeParameters:[n]})=>mU(Cx($e(n).check(uw()),r?.minSize,r?.maxSize)),toEquivalence:([r])=>wf(r)});return q(t.ast,{value:e})}function Gi(e){let t=qr()([e],([r])=>(n,o,s)=>wc(n)?fe(n)?aa:Vt(nt(r)(n.value,s),{onSuccess:O,onFailure:a=>Br(o,\"value\",a,n,s)}):P(new ft(o,n,s)),{representation:{id:\"effect/schema/Option\",payload:null},toCode:({typeParameters:r})=>({runtime:`Schema.Option(${r[0].runtime})`,Type:`Option.Option<${r[0].Type}>`,importDeclarations:['import * as Option from \"effect/Option\"']}),expected:\"Option\",toCodec:([r])=>Oe()(it([Me({_tag:Ct(\"Some\"),value:r}),Me({_tag:Ct(\"None\")})]),Pe({decode:n=>n._tag===\"None\"?v():O(n.value),encode:n=>be(n)?{_tag:\"Some\",value:n.value}:{_tag:\"None\"}}))});return q(t.ast,{value:e})}function s8(e){return rw(e).pipe(G(Gi(Hr(e)),dM()))}function a8(e){return Ax(e).pipe(G(Gi(Hr(e)),mM()))}function i8(e,t){return PU(e).pipe(G(Gi(Hr(e)),hM(t)))}var $D=Be(nP,{representation:{id:\"effect/http/Cookie\",payload:null},toCode:()=>({runtime:\"Schema.Cookie\",Type:\"Cookies.Cookie\",importDeclarations:['import * as Cookies from \"effect/http/Cookies\"']}),expected:\"Cookie\"}),BD=Be(rP,{representation:{id:\"effect/http/Cookies\",payload:null},toCode:()=>({runtime:\"Schema.Cookies\",Type:\"Cookies.Cookies\",importDeclarations:['import * as Cookies from \"effect/http/Cookies\"']}),expected:\"Cookies\",toCodecJson:()=>Oe()($e(K),Pe({decode:oP,encode:uP})),toCodecIso:()=>Oe()(_s(K,$D),Pe({decode:jE,encode:e=>e.cookies}))}),c8=BD.pipe(G(_s(K,K),Pe({decode:cP,encode:e=>zE(globalThis.Object.entries(e).map(([t,r])=>iP(t,r)))}))),u8=Be(dP,{representation:{id:\"effect/http/Headers\",payload:null},toCode:()=>({runtime:\"Schema.Headers\",Type:\"Headers.Headers\",importDeclarations:['import * as Headers from \"effect/http/Headers\"']}),expected:\"Headers\",toEquivalence:()=>KE,toCodec:()=>Oe()(_s(K,K),Pe({decode:mP,encode:e=>({...e})}))}),xw=Be(VE,{representation:{id:\"effect/http/UrlParams\",payload:null},toCode:()=>({runtime:\"Schema.UrlParams\",Type:\"UrlParams.UrlParams\",importDeclarations:['import * as UrlParams from \"effect/http/UrlParams\"']}),expected:\"UrlParams\",toEquivalence:()=>ZE,toCodec:()=>Oe()($e(Qo([K,K])),Pe({decode:Sh,encode:e=>e.params}))}),f8=(e,t)=>xw.pipe(G(lw(vp,t),Yt({decode:r=>Wt(gP(r,e),{onNone:()=>P(new ut([e],new ma(void 0))),onSome:g}),encode:r=>g(Sh([[e,r]]))}))),l8=xw.pipe(G(_s(K,it([K,tw(K)])),Pe({decode:yP,encode:hP})));function p8(e){return qi(e).pipe(G(Gi(Hr(e)),xM()))}function d8(e){return Lu(e).pipe(G(Gi(Hr(e)),gM()))}function m8(e,t){let r=t===void 0?\"omit\":t.onNoneEncoding,n=r===null?null:void 0;return Lu(rw(e)).pipe(G(Gi(Hr(e)),Ph({decode:o=>o.pipe(Va(cd),O),encode:r===\"omit\"?Ga:o=>O(Ff(Ga(o),()=>n))})))}function Fx(e,t){let r=typeof t?.label==\"string\"?t.label:void 0,n=t?.disallowJsonEncode===!0,o=r!==void 0?n?{label:r,disallowJsonEncode:!0}:{label:r}:n?{disallowJsonEncode:!0}:void 0,s=r!==void 0?nt(Ct(r)):void 0,a=qr()([e],([i])=>(c,u,f)=>{if(qE(c)){let p=s!==void 0?Ul(s(c.label,f),d=>new ut([\"label\"],d)):Q;return st(p,()=>Vt(nt(i)(au(c),f),{onSuccess:d=>ki(d,{label:c.label}),onFailure:()=>new wt(u,[new ut([\"value\"],new de(void 0,c,f))],c,f)}))}return P(new ft(u,c,f))},{representation:{id:\"effect/schema/Redacted\",payload:o??null},toCode:({typeParameters:i})=>({runtime:o!==void 0?`Schema.Redacted(${i[0].runtime}, ${F(o)})`:`Schema.Redacted(${i[0].runtime})`,Type:`Redacted.Redacted<${i[0].Type}>`,importDeclarations:['import * as Redacted from \"effect/Redacted\"']}),expected:\"Redacted\",toCodecJson:([i])=>Oe()(i,{decode:oe(c=>ki(c,{label:r})),encode:n?Ah(c=>\"Cannot serialize Redacted\"+(be(c)&&typeof c.value.label==\"string\"?` with label: \"${c.value.label}\"`:\"\")):oe(au)}),toEquivalence:([i])=>pP(i)});return q(a.ast,{value:e})}function h8(e,t){return G(Fx(Hr(e),{label:t?.label,disallowJsonEncode:t?.disallowEncode}),{decode:oe(r=>ki(r,{label:t?.label})),encode:t?.disallowEncode?Ah(r=>\"Cannot encode Redacted\"+(be(r)&&typeof r.value.label==\"string\"?` with label: \"${r.value.label}\"`:\"\")):oe(au)})(e)}function x8(e,t){let r=qr()([e,t],([n,o])=>(s,a,i)=>{if(!iA(s))return P(new ft(a,s,i));switch(s._tag){case\"Success\":return Vt(nT(n)(s.success,i),{onSuccess:B,onFailure:c=>Br(a,\"success\",c,s,i)});case\"Failure\":return Vt(nT(o)(s.failure,i),{onSuccess:W,onFailure:c=>Br(a,\"failure\",c,s,i)})}},{representation:{id:\"effect/schema/Result\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.Result(${n[0].runtime}, ${n[1].runtime})`,Type:`Result.Result<${n[0].Type}, ${n[1].Type}>`,importDeclarations:['import * as Result from \"effect/Result\"']}),expected:\"Result\",toCodec:([n,o])=>Oe()(it([Me({_tag:Ct(\"Success\"),success:n}),Me({_tag:Ct(\"Failure\"),failure:o})]),Pe({decode:s=>s._tag===\"Success\"?B(s.success):W(s.failure),encode:s=>mt(s)?{_tag:\"Success\",success:s.success}:{_tag:\"Failure\",failure:s.failure}}))});return q(r.ast,{success:e,failure:t})}var g8=globalThis.Symbol.for(\"immer-draftable\"),ZT={};function gw(e,t,r,n,o){let s=S8(r,t,n),a=jD(t),i=class extends e{constructor(...[c,u]){let p=u?.[\"~payload\"],d=p?.token===ZT?p.value:r.make(c??{},u);super(d,{...u,disableChecks:!0,\"~payload\":{token:ZT,value:d}})}static[Ap]=Ap;get[a](){return a}static[g8]=!0;static identifier=t;static fields=r.fields;static get ast(){return s(this).ast}static pipe(){return Z(this,arguments)}static rebuild(c){return s(this).rebuild(c)}static make(c,u){return Yh(s(this))(c??{},u)}static makeOption(c,u){return Zh(s(this))(c??{},u)}static makeEffect(c,u){return s(this).makeEffect(c??{},u)}static annotate(c){return this.rebuild(to(this.ast,c))}static annotateKey(c){return this.rebuild(cp(this.ast,c))}static check(...c){return this.rebuild(Vo(this.ast,c))}static extend(c){return(u,f)=>{let p=Fp(u)?u:Me(u),d={...r.fields,...p.fields},m=Lh(d,r.ast.checks,{identifier:c});return gw(this,c,ew(Vo(m,p.ast.checks),d),f,o)}}static mapFields(c,u){return r.mapFields(c,u)}};return o!==void 0&&Object.assign(i.prototype,o(t)),i}function y8(e){return new ve(oe(t=>new e(t,{\"~payload\":{token:ZT,value:t}})),jr())}function jD(e){return`~effect/Schema/Class/${e}`}function S8(e,t,r){let n;return o=>{if(n!==void 0)return n;let s=jD(t),a=u=>u instanceof o||k(u,s),i=y8(o),c=q(new yu([e.ast],()=>(u,f,p)=>a(u)?g(u):P(new ft(f,u,p)),{identifier:t,[Ih]:([u])=>({isConstructed:a,link:new at(u,i)}),toCodec:([u])=>new at(u.ast,i),toEquivalence:([u])=>u,toFormatter:([u])=>f=>`${o.identifier}(${u(f)})`,[Wl]:hu(e.ast),...r}));return n=G(c,i)(e)}}function Fp(e){return Tx(e)}var zD=e=>(t,r)=>{let n=Fp(t)?t:Me(t);return gw(Kd,e,n,r,o=>({toString(){return`${o}(${F({...this})})`}}))},b8=e=>(t,r,n)=>{let o=Fp(r)?r.mapFields(s=>({_tag:Hi(t),...s}),{unsafePreserveChecks:!0}):kx(t,r);return zD(e??t)(o,n)},qD=e=>(t,r)=>{let n=Fp(t)?t:Me(t);return gw(_f,e,n,r,s=>({name:s}))},E8=e=>(t,r,n)=>{let o=Fp(r)?r.mapFields(s=>({_tag:Hi(t),...s}),{unsafePreserveChecks:!0}):kx(t,r);return qD(e??t)(o,n)};function I8(e){return t=>t.annotate({toFormatter:e})}function T8(e,t){return _N(e.ast,t)}function w8(e){return t=>t.annotate({toEquivalence:e})}function C8(e){return bT(e.ast)}function A8(e,t){return mp(e.ast,t)}function k8(e,t){let r=mp(Cu(e.ast),t);return ix(r,t)}var v8=rx,O8=nx,Np=ox,_8=oN;function R8(e,t){return CN(e,t)}function P8(e){return DN(e)}function M8(e){return LN(e)}function F8(e){return $N(e)}function N8(e,t){return r=>q(to(r.ast,{toCodecIso:()=>new at(e.ast,Rh(t))}),{schema:r})}function U8(e){return wN(e)}function D8(e){let t=MU(()=>r),r=it([e,$e(t),_s(K,t)]);return r}var Nx=q(to(xu,{toCode:()=>({runtime:\"Schema.Json\",Type:\"Schema.Json\"})})),L8=_s(K,Nx),$8=Me({message:K,name:qi(K),stack:qi(K),cause:qi(Nx)}),B8=q(to(bF,{toCode:()=>({runtime:\"Schema.MutableJson\",Type:\"Schema.MutableJson\"})}));function j8(e){return As(e.ast)}function z8(e){return e.ast.context?.annotations}var HD=\"~effect/MutableHashMap\";var q8={[HD]:HD,[Symbol.iterator](){return this.backing[Symbol.iterator]()},toString(){return`MutableHashMap(${F(Array.from(this))})`},toJSON(){return{_id:\"MutableHashMap\",values:vt(Array.from(this))}},[Te](){return this.toJSON()},pipe(){return Z(this,arguments)}},Up=()=>{let e=Object.create(q8);return e.backing=new Map,e.buckets=new Map,e};var Xo=l(2,(e,t)=>{if(e.backing.has(t))return O(e.backing.get(t));if(yw(t))return v();let r=H(t),n=e.buckets.get(r);return n===void 0?v():H8(e,n,t)}),yw=e=>typeof e!=\"object\"&&typeof e!=\"function\";var H8=(e,t,r)=>{for(let n=0,o=t.length;n{if(e.backing.has(t)||yw(t))return e.backing.set(t,r),e;let n=H(t),o=e.buckets.get(n);if(o===void 0)return e.buckets.set(n,[t]),e.backing.set(t,r),e;let s=W8(o,t);return s===void 0&&(o.push(t),s=t),e.backing.set(s,r),e}),W8=(e,t)=>{for(let r=0,n=e.length;r{if(yw(t))return e.backing.delete(t),e;let r=H(t),n=e.buckets.get(r);if(n===void 0)return e;for(let o=0,s=n.length;o(e.backing.clear(),e.buckets.clear(),e),WD=e=>e.backing.size;var JD=Qy;var Ea=Xy,KD=hk,mo=rl;var Zu=ds,Ia=dO;var Pr=JS,GD=bO;var VD=\"~effect/MutableRef\",J8={[VD]:VD,...rr,toJSON(){return{_id:\"MutableRef\",current:vt(this.current)}}},Yu=e=>{let t=Object.create(J8);return t.current=e,t};var ZD=e=>e.current;var Lx=l(2,(e,t)=>(e.current=t,e));var Xt=Symbol.for(\"effect/MutableList/Empty\"),Ps=()=>({head:void 0,tail:void 0,length:0}),Ew=e=>{let t=e.head;t.offset>=1024&&t===e.tail&&t.mutable&&(t.array.length-t.offset)*8<=t.offset&&(e.head=e.tail={array:t.array.slice(t.offset),mutable:!0,offset:0,next:void 0})},YD=()=>({array:[],mutable:!0,offset:0,next:void 0}),ho=(e,t)=>{e.tail?e.tail.mutable||(e.tail.next=YD(),e.tail=e.tail.next):e.head=e.tail=YD(),e.tail.array.push(t),e.length++},Iw=(e,t)=>{e.head={array:[t],mutable:!0,offset:0,next:e.head},e.tail||(e.tail=e.head),e.length++};var $x=(e,t)=>Bx(e,Ye(t),!Array.isArray(t)),Bx=(e,t,r=!1)=>{if(t.length===0)return 0;let n={array:t,mutable:r,offset:0,next:void 0};return e.head?e.tail=e.tail.next=n:e.head=e.tail=n,e.length+=t.length,t.length},Rs=e=>{e.head=e.tail=void 0,e.length=0},Vi=(e,t)=>{if(t=ot(t),t<=0||!e.head)return[];if(t=Math.min(t,e.length),t===e.length&&e.head?.offset===0&&!e.head.next){let s=e.head.array;return Rs(e),s}let r=new Array(t),n=0,o=e.head;for(;o;){for(;o.offset{if(t=ot(t),t<=0||!e.head)return;if(t=Math.min(t,e.length),t===e.length&&e.head?.offset===0&&!e.head.next){Rs(e);return}let r=0,n=e.head;for(;n;){let o=n.array.length-n.offset;if(r+o>t){n.offset+=t-r,e.head=n,e.length-=t,Ew(e);return}r+=o,n=n.next}Rs(e)},Ta=e=>Vi(e,e.length),Zi=e=>{if(!e.head)return Xt;let t=e.head.array[e.head.offset];return e.head.mutable&&(e.head.array[e.head.offset]=void 0),e.head.offset++,e.length--,e.head.offset===e.head.array.length?e.head.next?e.head=e.head.next:Rs(e):Ew(e),t},QD=(e,t)=>{if(t=ot(t),t<=0)return[];let r=Math.min(t,e.length),n=new Array(r),o=0,s=e.head;for(;s;){for(let a=s.offset;a{let r=[],n=e.head,o=0;for(;n;){for(let s=n.offset;sTw(e,r=>r!==t);var K8=\"~effect/PubSub\";var G8=\"~effect/PubSub/Subscription\",Jx=e=>X(()=>eY(e.atomicPubSub(),new Map,kn(),Pr(!1),Yu(!1),e.strategy(),Yu(v()))),Kx=e=>Jx({atomicPubSub:()=>Nw(e),strategy:()=>new qx}),Gx=e=>Jx({atomicPubSub:()=>Nw(e),strategy:()=>new Hx}),Vx=e=>Jx({atomicPubSub:()=>Nw(e),strategy:()=>new Fw}),Zx=e=>Jx({atomicPubSub:()=>e1(e),strategy:()=>new Hx}),Nw=e=>{let t=typeof e==\"number\"?{capacity:e}:e;if(tY(t.capacity),t.capacity===1/0)return e1(t);let r=t.replay&&t.replay>0?new Wx(Math.ceil(t.replay)):void 0;return t.capacity===1?new vw(r):T_(t.capacity)===t.capacity?new Aw(t.capacity,r):new ww(t.capacity,r)},e1=e=>{let t=e?.replay;return new _w(t&&t>0?new Wx(Math.ceil(t)):void 0)};var $p=e=>Ts(cn(t=>(Lx(e.shutdownFlag,!0),Le(e.scope,pb(t.id)).pipe(_r(e.strategy.shutdown),nh(e.shutdownHook.open),ia)))),Yx=l(2,(e,t)=>X(()=>V8(e,t))),V8=l(2,(e,t)=>{if(e.shutdownFlag.current||be(e.ended.current))return!1;if(Lx(e.ended,O(t)),e.strategy instanceof qx)for(let[n,o,s]of Ta(e.strategy.publishers))s&&Do(o,vr(!1));let r=vr(t);for(let n of e.subscribers.values())for(let o of n){let s;for(;(s=Zi(o))!==Xt;)Do(s,r)}return e.subscribers.clear(),!0});var Qx=l(2,(e,t)=>$(()=>e.shutdownFlag.current||be(e.ended.current)?g(!1):e.pubsub.publish(t)?(e.strategy.completeSubscribersUnsafe(e.pubsub,e.subscribers),g(!0)):e.strategy.handleSurplus(e.pubsub,e.subscribers,[t],e.shutdownFlag,e.ended)));var t1=l(2,(e,t)=>$(()=>{if(e.shutdownFlag.current||be(e.ended.current))return g(!1);let r=e.pubsub.publishAll(t);return e.strategy.completeSubscribersUnsafe(e.pubsub,e.subscribers),r.length===0?g(!0):e.strategy.handleSurplus(e.pubsub,e.subscribers,r,e.shutdownFlag,e.ended)})),Bp=e=>Ts(Ml(t=>{let r=Fe(t,Or),n=Nt(e.scope),o=X8(e.pubsub,e.subscribers,e.strategy,e.ended);return Kt(n,Z8(o)).pipe(_r(Lo(r,s=>Le(n,s))),Tt(o))})),Z8=e=>Ts(cn(t=>(Lx(e.shutdownFlag,!0),sa(Ta(e.pollers),r=>xb(r,t.id),{discard:!0,concurrency:\"unbounded\"}).pipe(Rr(()=>X(()=>{e.subscribers.delete(e.subscription),e.subscription.unsubscribe(),e.replayWindow.close(),e.strategy.onPubSubEmptySpaceUnsafe(e.pubsub,e.subscribers)})),nh(e.shutdownHook.open),ia)))),Xx=e=>$(()=>{if(e.shutdownFlag.current)return _n;if(e.replayWindow.remaining>0){let r=e.replayWindow.take();return g(r)}let t=e.pollers.length===0?e.subscription.poll():Xt;return t===Xt?n1(e):(e.strategy.onPubSubEmptySpaceUnsafe(e.pubsub,e.subscribers),g(t))}),r1=e=>$(function t(r){if(e.shutdownFlag.current)return _n;let n=e.pollers.length===0?e.subscription.pollUpTo(Number.POSITIVE_INFINITY):[];return r&&(n=r.concat(n)),e.strategy.onPubSubEmptySpaceUnsafe(e.pubsub,e.subscribers),e.replayWindow.remaining>0?g(e.replayWindow.takeAll().concat(n)):or(n)?g(n):I(n1(e),o=>t([o]))}),n1=e=>Yc(t=>{if(e.shutdownFlag.current)return t(_n);if(be(e.ended.current)){let o=e.pollers.length===0?e.subscription.poll():Xt;return o!==Xt?(e.strategy.onPubSubEmptySpaceUnsafe(e.pubsub,e.subscribers),t(g(o))):t(g(e.ended.current.value))}let r=ui(),n=e.subscribers.get(e.subscription);return n||(n=new Set,e.subscribers.set(e.subscription,n)),n.add(e.pollers),ho(e.pollers,r),e.strategy.completePollersUnsafe(e.pubsub,e.subscribers,e.subscription,e.pollers),r.effect?t(r.effect):(r.resumes=[t],X(()=>XD(e.pollers,r)))});var bt=Symbol.for(\"effect/PubSub/AbsentValue\"),Y8=(e,t,r)=>{e.has(t)||e.set(t,new Set),e.get(t).add(r)},Q8=(e,t,r)=>{if(!e.has(t))return;let n=e.get(t);n.delete(r),n.size===0&&e.delete(t)},X8=(e,t,r,n)=>new Pw(e,t,e.subscribe(),Ps(),Pr(!1),Yu(!1),r,e.replayWindow(),n),ww=class{array;replayIndices;publisherIndex=0;subscribers;subscriberCount=0;subscribersIndex=0;capacity;replayBuffer;constructor(t,r){this.capacity=t,this.replayBuffer=r,this.array=Array.from({length:t}),this.replayIndices=r?Array.from({length:t}):[],this.subscribers=Array.from({length:t})}replayWindow(){return this.replayBuffer?new Qu(this.replayBuffer):eg}isEmpty(){return this.publisherIndex===this.subscribersIndex}isFull(){return this.publisherIndex===this.subscribersIndex+this.capacity}size(){return this.publisherIndex-this.subscribersIndex}publish(t){if(this.isFull())return!1;let r=this.replayBuffer?.offer(t);if(this.subscriberCount!==0){let n=this.publisherIndex%this.capacity;this.array[n]=t,r!==void 0&&(this.replayIndices[n]=r),this.subscribers[n]=this.subscriberCount,this.publisherIndex+=1}return!0}publishAll(t){if(this.subscriberCount===0)return this.replayBuffer&&this.replayBuffer.offerAll(t),[];let r=Ye(t),n=r.length,o=this.publisherIndex-this.subscribersIndex,s=this.capacity-o,a=Math.min(n,s);if(a===0)return r;let i=0,c=this.publisherIndex+a;for(;this.publisherIndex!==c;){let u=r[i++],f=this.publisherIndex%this.capacity;this.array[f]=u;let p=this.replayBuffer?.offer(u);p!==void 0&&(this.replayIndices[f]=p),this.subscribers[f]=this.subscriberCount,this.publisherIndex+=1}return r.slice(i)}slide(){if(this.subscribersIndex!==this.publisherIndex){let t=this.subscribersIndex%this.capacity,r=this.array[t];this.array[t]=bt,this.subscribers[t]=0,this.subscribersIndex+=1,this.replayBuffer?.slide(r,this.replayIndices[t])}}subscribe(){return this.subscriberCount+=1,new Cw(this,this.publisherIndex,!1)}},Cw=class{self;subscriberIndex;unsubscribed;constructor(t,r,n){this.self=t,this.subscriberIndex=r,this.unsubscribed=n}isEmpty(){return this.unsubscribed||this.self.publisherIndex===this.subscriberIndex||this.self.publisherIndex===this.self.subscribersIndex}size(){return this.unsubscribed?0:this.self.publisherIndex-Math.max(this.subscriberIndex,this.self.subscribersIndex)}poll(){if(this.unsubscribed)return Xt;if(this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex),this.subscriberIndex!==this.self.publisherIndex){let t=this.subscriberIndex%this.self.capacity,r=this.self.array[t];return this.self.subscribers[t]-=1,this.self.subscribers[t]===0&&(this.self.array[t]=bt,this.self.subscribersIndex+=1),this.subscriberIndex+=1,r}return Xt}pollUpTo(t){if(t=ot(t),this.unsubscribed)return[];this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex);let r=this.self.publisherIndex-this.subscriberIndex,n=Math.min(t,r);if(n<=0)return[];let o=[],s=this.subscriberIndex+n;for(;this.subscriberIndex!==s;){let a=this.subscriberIndex%this.self.capacity,i=this.self.array[a];this.self.subscribers[a]-=1,this.self.subscribers[a]===0&&(this.self.array[a]=bt,this.self.subscribersIndex+=1),o.push(i),this.subscriberIndex+=1}return o}unsubscribe(){if(!this.unsubscribed)for(this.unsubscribed=!0,this.self.subscriberCount-=1,this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex);this.subscriberIndex!==this.self.publisherIndex;){let t=this.subscriberIndex%this.self.capacity;this.self.subscribers[t]-=1,this.self.subscribers[t]===0&&(this.self.array[t]=bt,this.self.subscribersIndex+=1),this.subscriberIndex+=1}}},Aw=class{array;replayIndices;mask;publisherIndex=0;subscribers;subscriberCount=0;subscribersIndex=0;capacity;replayBuffer;constructor(t,r){this.capacity=t,this.replayBuffer=r,this.array=Array.from({length:t}),this.replayIndices=r?Array.from({length:t}):[],this.mask=t-1,this.subscribers=Array.from({length:t})}replayWindow(){return this.replayBuffer?new Qu(this.replayBuffer):eg}isEmpty(){return this.publisherIndex===this.subscribersIndex}isFull(){return this.publisherIndex===this.subscribersIndex+this.capacity}size(){return this.publisherIndex-this.subscribersIndex}publish(t){if(this.isFull())return!1;let r=this.replayBuffer?.offer(t);if(this.subscriberCount!==0){let n=this.publisherIndex&this.mask;this.array[n]=t,r!==void 0&&(this.replayIndices[n]=r),this.subscribers[n]=this.subscriberCount,this.publisherIndex+=1}return!0}publishAll(t){if(this.subscriberCount===0)return this.replayBuffer&&this.replayBuffer.offerAll(t),[];let r=Ye(t),n=r.length,o=this.publisherIndex-this.subscribersIndex,s=this.capacity-o,a=Math.min(n,s);if(a===0)return r;let i=0,c=this.publisherIndex+a;for(;this.publisherIndex!==c;){let u=r[i++],f=this.publisherIndex&this.mask;this.array[f]=u;let p=this.replayBuffer?.offer(u);p!==void 0&&(this.replayIndices[f]=p),this.subscribers[f]=this.subscriberCount,this.publisherIndex+=1}return r.slice(i)}slide(){if(this.subscribersIndex!==this.publisherIndex){let t=this.subscribersIndex&this.mask,r=this.array[t];this.array[t]=bt,this.subscribers[t]=0,this.subscribersIndex+=1,this.replayBuffer?.slide(r,this.replayIndices[t])}}subscribe(){return this.subscriberCount+=1,new kw(this,this.publisherIndex,!1)}},kw=class{self;subscriberIndex;unsubscribed;constructor(t,r,n){this.self=t,this.subscriberIndex=r,this.unsubscribed=n}isEmpty(){return this.unsubscribed||this.self.publisherIndex===this.subscriberIndex||this.self.publisherIndex===this.self.subscribersIndex}size(){return this.unsubscribed?0:this.self.publisherIndex-Math.max(this.subscriberIndex,this.self.subscribersIndex)}poll(){if(this.unsubscribed)return Xt;if(this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex),this.subscriberIndex!==this.self.publisherIndex){let t=this.subscriberIndex&this.self.mask,r=this.self.array[t];return this.self.subscribers[t]-=1,this.self.subscribers[t]===0&&(this.self.array[t]=bt,this.self.subscribersIndex+=1),this.subscriberIndex+=1,r}return Xt}pollUpTo(t){if(t=ot(t),this.unsubscribed)return[];this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex);let r=this.self.publisherIndex-this.subscriberIndex,n=Math.min(t,r);if(n<=0)return[];let o=[],s=this.subscriberIndex+n;for(;this.subscriberIndex!==s;){let a=this.subscriberIndex&this.self.mask,i=this.self.array[a];this.self.subscribers[a]-=1,this.self.subscribers[a]===0&&(this.self.array[a]=bt,this.self.subscribersIndex+=1),o.push(i),this.subscriberIndex+=1}return o}unsubscribe(){if(!this.unsubscribed)for(this.unsubscribed=!0,this.self.subscriberCount-=1,this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex);this.subscriberIndex!==this.self.publisherIndex;){let t=this.subscriberIndex&this.self.mask;this.self.subscribers[t]-=1,this.self.subscribers[t]===0&&(this.self.array[t]=bt,this.self.subscribersIndex+=1),this.subscriberIndex+=1}}},vw=class{publisherIndex=0;subscriberCount=0;subscribers=0;value=bt;replayIndex=0;capacity=1;replayBuffer;constructor(t){this.replayBuffer=t}replayWindow(){return this.replayBuffer?new Qu(this.replayBuffer):eg}pipe(){return Z(this,arguments)}isEmpty(){return this.subscribers===0}isFull(){return!this.isEmpty()}size(){return this.isEmpty()?0:1}publish(t){if(this.isFull())return!1;let r=this.replayBuffer?.offer(t);return this.subscriberCount!==0&&(this.value=t,r!==void 0&&(this.replayIndex=r),this.subscribers=this.subscriberCount,this.publisherIndex+=1),!0}publishAll(t){if(this.subscriberCount===0)return this.replayBuffer&&this.replayBuffer.offerAll(t),[];let r=Ye(t);return r.length===0?r:this.publish(r[0])?r.slice(1):r}slide(){if(this.isFull()){let t=this.value;this.subscribers=0,this.value=bt,this.replayBuffer?.slide(t,this.replayIndex)}}subscribe(){return this.subscriberCount+=1,new Ow(this,this.publisherIndex,!1)}},Ow=class{self;subscriberIndex;unsubscribed;constructor(t,r,n){this.self=t,this.subscriberIndex=r,this.unsubscribed=n}isEmpty(){return this.unsubscribed||this.self.subscribers===0||this.subscriberIndex===this.self.publisherIndex}size(){return this.isEmpty()?0:1}poll(){if(this.isEmpty())return Xt;let t=this.self.value;return this.self.subscribers-=1,this.self.subscribers===0&&(this.self.value=bt),this.subscriberIndex=this.self.publisherIndex,t}pollUpTo(t){return ot(t)<1||this.isEmpty()?[]:[this.poll()]}unsubscribe(){this.unsubscribed||(this.unsubscribed=!0,this.self.subscriberCount-=1,this.self.subscribers!==0&&this.subscriberIndex!==this.self.publisherIndex&&(this.self.subscribers-=1,this.self.subscribers===0&&(this.self.value=bt)))}},_w=class{publisherHead={value:bt,replayIndex:void 0,subscribers:0,next:null};publisherTail=this.publisherHead;publisherIndex=0;subscribersIndex=0;capacity=Number.MAX_SAFE_INTEGER;replayBuffer;constructor(t){this.replayBuffer=t}replayWindow(){return this.replayBuffer?new Qu(this.replayBuffer):eg}isEmpty(){return this.publisherHead===this.publisherTail}isFull(){return!1}size(){return this.publisherIndex-this.subscribersIndex}publish(t){let r=this.replayBuffer?.offer(t),n=this.publisherTail.subscribers;if(n!==0){let o={value:t,replayIndex:r,subscribers:n,next:null};this.publisherTail.next=o,this.publisherTail=this.publisherTail.next,this.publisherIndex+=1}return!0}publishAll(t){if(this.publisherTail.subscribers!==0)for(let r of t)this.publish(r);else this.replayBuffer&&this.replayBuffer.offerAll(t);return[]}slide(){if(this.publisherHead!==this.publisherTail){let t=this.publisherHead.next,r=t.value;this.publisherHead=this.publisherHead.next,this.publisherHead.value=bt,this.subscribersIndex+=1,this.replayBuffer?.slide(r,t.replayIndex)}}subscribe(){return this.publisherTail.subscribers+=1,new Rw(this,this.publisherTail,this.publisherIndex,!1)}},Rw=class{self;subscriberHead;subscriberIndex;unsubscribed;constructor(t,r,n,o){this.self=t,this.subscriberHead=r,this.subscriberIndex=n,this.unsubscribed=o}isEmpty(){if(this.unsubscribed)return!0;let t=!0,r=!0;for(;r;)this.subscriberHead===this.self.publisherTail?r=!1:this.subscriberHead.next.value!==bt?(t=!1,r=!1):(this.subscriberHead=this.subscriberHead.next,this.subscriberIndex+=1);return t}size(){return this.unsubscribed?0:this.self.publisherIndex-Math.max(this.subscriberIndex,this.self.subscribersIndex)}poll(){if(this.unsubscribed)return Xt;let t=!0,r=Xt;for(;t;)if(this.subscriberHead===this.self.publisherTail)t=!1;else{let n=this.subscriberHead.next.value;n!==bt&&(r=n,this.subscriberHead.subscribers-=1,this.subscriberHead.subscribers===0&&(this.self.publisherHead=this.self.publisherHead.next,this.self.publisherHead.value=bt,this.self.subscribersIndex+=1),t=!1),this.subscriberHead=this.subscriberHead.next,this.subscriberIndex+=1}return r}pollUpTo(t){t=ot(t);let r=[],n=0;for(;n!==t;){let o=this.poll();o===Xt?n=t:(r.push(o),n+=1)}return r}unsubscribe(){if(!this.unsubscribed)for(this.unsubscribed=!0,this.self.publisherTail.subscribers-=1;this.subscriberHead!==this.self.publisherTail;)this.subscriberHead.next.value!==bt&&(this.subscriberHead.subscribers-=1,this.subscriberHead.subscribers===0&&(this.self.publisherHead=this.self.publisherHead.next,this.self.publisherHead.value=bt,this.self.subscribersIndex+=1)),this.subscriberHead=this.subscriberHead.next}},Pw=class{[G8]={_A:R};pubsub;subscribers;subscription;pollers;shutdownHook;shutdownFlag;strategy;replayWindow;ended;constructor(t,r,n,o,s,a,i,c,u){this.pubsub=t,this.subscribers=r,this.subscription=n,this.pollers=o,this.shutdownHook=s,this.shutdownFlag=a,this.strategy=i,this.replayWindow=c,this.ended=u}pipe(){return Z(this,arguments)}},Mw=class{[K8]={_A:R};pubsub;subscribers;scope;shutdownHook;shutdownFlag;strategy;ended;constructor(t,r,n,o,s,a,i){this.pubsub=t,this.subscribers=r,this.scope=n,this.shutdownHook=o,this.shutdownFlag=s,this.strategy=a,this.ended=i}pipe(){return Z(this,arguments)}},eY=(e,t,r,n,o,s,a)=>new Mw(e,t,r,n,o,s,a),tY=e=>{if(e<=0||e!==1/0&&!Number.isInteger(e))throw new Error(`Cannot construct PubSub with capacity of ${e}`)},qx=class{publishers=Ps();get shutdown(){return cn(t=>sa(Ta(this.publishers),([r,n,o])=>o?xb(n,t.id):Q,{concurrency:\"unbounded\",discard:!0}))}handleSurplus(t,r,n,o,s){return Yc(a=>{if(be(s.current))return a(g(!1));let i=ui();return this.offerUnsafe(n,i),this.onPubSubEmptySpaceUnsafe(t,r),this.completeSubscribersUnsafe(t,r),ZD(o)?(this.removeUnsafe(i),a(_n)):i.effect?a(i.effect):(i.resumes=[a],X(()=>this.removeUnsafe(i)))})}onPubSubEmptySpaceUnsafe(t,r){let n=!0;for(;n&&!t.isFull();){let o=Zi(this.publishers);if(o===Xt)n=!1;else{let[s,a]=o,i=t.publish(s);i&&o[2]?Do(a,vr(!0)):i||Iw(this.publishers,o),this.completeSubscribersUnsafe(t,r)}}}completePollersUnsafe(t,r,n,o){return Uw(this,t,r,n,o)}completeSubscribersUnsafe(t,r){return Dw(this,t,r)}offerUnsafe(t,r){let n=t[Symbol.iterator](),o=n.next();if(!o.done)for(;;){let s=o.value;if(o=n.next(),o.done){ho(this.publishers,[s,r,!0]);break}ho(this.publishers,[s,r,!1])}}removeUnsafe(t){Tw(this.publishers,([r,n])=>n!==t)}},Hx=class{get shutdown(){return Q}handleSurplus(t,r,n,o){return g(!1)}onPubSubEmptySpaceUnsafe(t,r){}completePollersUnsafe(t,r,n,o){return Uw(this,t,r,n,o)}completeSubscribersUnsafe(t,r){return Dw(this,t,r)}},Fw=class{get shutdown(){return Q}handleSurplus(t,r,n,o,s){return X(()=>be(s.current)?!1:(this.slidingPublishUnsafe(t,n),this.completeSubscribersUnsafe(t,r),!0))}onPubSubEmptySpaceUnsafe(t,r){}completePollersUnsafe(t,r,n,o){return Uw(this,t,r,n,o)}completeSubscribersUnsafe(t,r){return Dw(this,t,r)}slidingPublishUnsafe(t,r){let n=r[Symbol.iterator](),o=n.next();if(!o.done&&t.capacity>0){let s=o.value,a=!0;for(;a;){t.slide();let i=t.publish(s);i&&(o=n.next())&&!o.done?s=o.value:i&&(a=!1)}}}},Uw=(e,t,r,n,o)=>{let s=!0;for(;s&&!n.isEmpty();){let a=Zi(o);if(a===Xt)Q8(r,n,o),o.length===0?s=!1:Y8(r,n,o);else{let i=n.poll();i===Xt?Iw(o,a):(Do(a,vr(i)),e.onPubSubEmptySpaceUnsafe(t,r))}}},Dw=(e,t,r)=>{for(let[n,o]of r)for(let s of o)e.completePollersUnsafe(t,r,n,s)},Wx=class{capacity;head={value:bt,index:0,next:null};tail=this.head;slideValues=[];size=0;index=0;publisherIndex=0;constructor(t){this.capacity=t}slide(t,r){this.slideValues[this.index%this.capacity]={value:t,index:r},this.index++}offer(t){let r=this.publisherIndex++;return this.tail.value=t,this.tail.index=r,this.tail.next={value:bt,index:0,next:null},this.tail=this.tail.next,this.size===this.capacity?this.head=this.head.next:this.size+=1,r}offerAll(t){for(let r of t)this.offer(r)}},Qu=class{buffer;values;index=0;remaining;slideIndex;newestIndex=-1;constructor(t){this.buffer=t,this.remaining=t.size,this.slideIndex=t.index,this.values=new Array(this.remaining);let r=t.head;for(let n=0;nthis.newestIndex&&(this.index=(this.index+1)%this.values.length,this.values[(this.index+this.remaining-1)%this.values.length]=s.value,this.newestIndex=s.index)}this.slideIndex=this.buffer.index}take(){if(this.remaining===0)return;this.sync();let t=this.values[this.index];return this.values[this.index]=bt,this.index=(this.index+1)%this.values.length,this.remaining--,this.remaining===0&&this.close(),t}takeN(t){t=ot(t);let r=Math.min(t,this.remaining),n=new Array(r);for(let o=0;o{},takeN:()=>[],takeAll:()=>[],close:()=>{}};var rY=\"~effect/Queue\",nY=\"~effect/Queue/Enqueue\",oY=\"~effect/Queue/Dequeue\";var $w={_A:R,_E:R},sY={[rY]:$w,[nY]:$w,[oY]:$w,...rr,toJSON(){return{_id:\"effect/Queue\",state:this.state._tag,size:cY(this)}}},Mn=e=>re(t=>{let r=Object.create(sY);return r.dispatcher=t.currentDispatcher,r.capacity=e?.capacity??Number.POSITIVE_INFINITY,r.strategy=e?.strategy??\"suspend\",r.messages=Ps(),r.scheduleRunning=!1,r.state={_tag:\"Open\",takers:new Set,offers:new Set,awaiters:new Set},D(r)}),jp=e=>Mn({capacity:e});var o1=()=>Mn(),Sn=l(2,(e,t)=>ne(()=>rg(e,t)?sg:e.state._tag===\"Open\"&&e.strategy===\"suspend\"?pY(e,t):a1)),rg=(e,t)=>e.state._tag!==\"Open\"?!1:e.messages.length>=e.capacity?e.strategy===\"sliding\"?(Zi(e.messages),ho(e.messages,t),!0):e.capacity<=0&&e.state.takers.size>0?(ho(e.messages,t),i1(e),!0):!1:(ho(e.messages,t),tg(e),!0),Yi=l(2,(e,t)=>ht(r=>{let n=ng(e,t);return n.length===0||e.strategy===\"dropping\"?r(Ae(n)):f1(e,{_tag:\"Array\",remaining:n,offset:0,resume:r})})),ng=(e,t)=>{if(e.state._tag!==\"Open\")return Ye(t);if(e.capacity===Number.POSITIVE_INFINITY||e.strategy===\"sliding\")return $x(e.messages,t),e.strategy===\"sliding\"&&Vi(e.messages,e.messages.length-e.capacity),tg(e),[];let r=e.capacity<=0?e.state.takers.size:e.capacity-e.messages.length;if(r===0)return Ye(t);let n=[],o=0;for(let s of t)oz(()=>wa(e,t))),wa=(e,t)=>{if(e.state._tag!==\"Open\")return!1;let r=Ht(t),n=Jk(r,uY);return e.state.offers.size===0&&e.messages.length===0?(ag(e,n),!0):(e.state={...e.state,_tag:\"Closing\",exit:n},tg(e),!0)},jw=e=>Bt(e,gc(Wa())),og=e=>wa(e,gc(Wa()));var Fn=e=>z(()=>aY(e)),aY=e=>{if(e.state._tag===\"Done\")return!1;Rs(e.messages);let t=e.state.offers;ag(e,e.state._tag===\"Open\"?fY:e.state.exit);for(let r of t)l1(r);return!0};var Qi=e=>s1(e,1,Number.POSITIVE_INFINITY);var s1=l(3,(e,t,r)=>(t=ot(t),r=ot(r),c1(()=>lY(e,t,r)??Fo(u1(e,()=>zw(e,t)),s1(e,t,r))))),Ms=e=>c1(()=>iY(e)??Fo(u1(e,()=>zw(e,1)),Ms(e)));var iY=e=>{if(e.state._tag===\"Done\")return e.state.exit;if(e.messages.length>0){let t=Zi(e.messages);return Bw(e),Ae(t)}else if(e.capacity<=0&&e.state.offers.size>0){let t=p1(e.state.offers);return Bw(e),Ae(t)}};var cY=e=>e.state._tag===\"Done\"?0:e.messages.length;var a1=Ae(!1),sg=Ae(!0),uY=en(Wa()),fY=Nc(),i1=e=>{if(!(e.state._tag===\"Done\"||e.state.takers.size===0)){for(let t of e.state.takers)if(t.ready()&&(e.state.takers.delete(t),t.resume(pr),e.messages.length===0))break}},tg=e=>{e.scheduleRunning||e.state._tag===\"Done\"||e.state.takers.size===0||(e.scheduleRunning=!0,e.dispatcher.scheduleTask(()=>{e.scheduleRunning=!1,i1(e)},0))},lY=(e,t,r)=>{if(e.state._tag===\"Done\")return e.state.exit;if(r<=0||t<=0)return Ae([]);if(!zw(e,t))return;let n=e.messages.length>0?Vi(e.messages,r):[p1(e.state.offers)];return Bw(e),Ae(n)},c1=En({op:\"QueueTake\",[Ke](e){let t=this[ue]();return vf(t)?t[Ke](e):t}}),zw=(e,t)=>e.messages.length>=(e.state._tag===\"Closing\"?1:Math.min(t,e.capacity||1))||e.capacity<=0&&e.state._tag!==\"Done\"&&e.state.offers.size>0,u1=(e,t)=>ht(r=>{if(e.state._tag===\"Done\")return r(e.state.exit);if(t())return r(pr);let n={ready:t,resume:r};return e.state.takers.add(n),z(()=>{e.state._tag!==\"Done\"&&e.state.takers.delete(n)})}),pY=(e,t)=>ht(r=>rg(e,t)?r(sg):f1(e,{_tag:\"Single\",message:t,resume:r})),f1=(e,t)=>{if(e.state._tag!==\"Open\")return l1(t);let r=e.state.offers;return r.add(t),z(()=>{e.state._tag!==\"Done\"&&(r.delete(t),e.state._tag===\"Closing\"&&r.size===0&&e.messages.length===0&&ag(e,e.state.exit))})},l1=e=>e._tag===\"Single\"?e.resume(a1):e.resume(Ae(e.remaining.slice(e.offset))),p1=e=>{let t=e.values().next().value;if(t._tag===\"Single\")return e.delete(t),t.resume(sg),t.message;let r=t.remaining[t.offset++];return t.offset===t.remaining.length&&(e.delete(t),t.resume(Ae([]))),r},Bw=e=>{if(e.state._tag===\"Done\")return Bo(e.state.exit.cause);if(e.state.offers.size===0)return e.state._tag===\"Closing\"&&e.messages.length===0?(ag(e,e.state.exit),Bo(e.state.exit.cause)):!1;for(let t of e.state.offers){let r=e.capacity-e.messages.length;if(r<=0)break;if(t._tag===\"Single\")ho(e.messages,t.message),e.state.offers.delete(t),t.resume(sg);else{for(;t.offset{if(e.state._tag===\"Done\")return;let r=e.state;e.state={_tag:\"Done\",exit:t};for(let n of r.takers)n.resume(t);r.takers.clear();for(let n of r.awaiters)n(t);r.awaiters.clear()};var ig=e=>new Hw(e),d1=(e,t,r)=>ht(n=>{if(e.free>=t)return n(r);let o=()=>{e.free{e.waiters.delete(o)})}),Hw=class{waiters=new Set;taken=0;permits;constructor(t){this.permits=t}get free(){return this.permits-this.taken}take(t){let r=ne(()=>this.freethis.free0&&t.currentDispatcher.scheduleTask(()=>{for(let n of this.waiters){if(this.free<=0)break;n()}},0),this.free}resize(t){return re(r=>(this.permits=t,this.free<0?pe:(this.releaseUnsafe(r,0),pe)))}release(t){return re(r=>D(this.releaseUnsafe(r,t)))}get releaseAll(){return re(t=>D(this.releaseUnsafe(t,this.taken)))}withPermits(t){return r=>Hn(n=>{let o=ne(()=>{if(this.freeo)}return this.taken+=t,qn(n(r),()=>{this.releaseUnsafe(ds(),t)},!0)});return o})}withPermit=this.withPermits(1);withPermitsIfAvailable(t){return r=>Hn(n=>this.free{this.releaseUnsafe(ds(),t)},!0)))}};var Ww=e=>hl(e)?Ve(e)?Y(e.value):e:g(e);var y1=\"~effect/Channel\",cr=e=>k(e,y1),dY={[y1]:{_Env:R,_InErr:R,_InElem:R,_OutErr:R,_OutElem:R},pipe(){return Z(this,arguments)}},Ce=e=>{let t=Object.create(dY);return t.transform=(r,n)=>rt(e(r,n),o=>g(ze(o))),t},Nn=(e,t)=>Ce((r,n)=>I(he(e)(r,n),o=>t(o,n))),er=e=>Ce((t,r)=>e),go=e=>Ce(Se(function*(t,r){let n=Nt(r),o=a=>Le(n,Jm(a)),s=yield*Vn(e(t,r,n),o);return Vn(s,o)})),he=e=>e.transform,zp=4096,mY=(e,t,r)=>Mn({capacity:r?.bufferSize,strategy:r?.strategy}).pipe(Rr(n=>Kt(e,Fn(n))),Rr(n=>Es(t(n),e).pipe(rt(o=>Bt(n,o)),Ut(e))));var S1=(e,t)=>Ce((r,n)=>j(mY(n,e,t),Qi)),cg=e=>Ce((t,r)=>$(()=>he(e())(t,r))),hY=(e,t,r)=>go(Se(function*(n,o,s){let a=v();yield*Lo(s,c=>be(a)?r(a.value,c):Q);let i=yield*Ts(j(e,c=>(a=O(c),c)));return yield*he(t(i))(n,o)}));var b1=e=>er(X(()=>{let t=e();return $(()=>{let r=t.next();return r.done?Y(r.value):g(r.value)})})),Jw=e=>er(X(()=>{let t=0;return $(()=>t>=e.length?Y():g(e[t++]))}));var Kw=(e,t=zp)=>{let r=Tn(t);return er(X(()=>{let n=e(),o=v();return $(()=>{if(o._tag===\"Some\")return Y(o.value);let s=[];for(;s.lengthKw(()=>e[Symbol.iterator](),t),ug=e=>fg(g(e));var I1=e=>fg(X(e)),Xu=er(g(Y())),T1=er(g(mi)),es=e=>er(g(P(e))),w1=e=>er(Xm(e)),qp=e=>er(ze(e)),C1=e=>er(Is(e)),Gw=e=>qp(ci(e)),fg=e=>er(X(()=>{let t=!1;return $(()=>t?Y():(t=!0,e))}));var A1=e=>er(I(e,()=>Y())),Vw=e=>er(g(I(e,Ww)));var k1=e=>er(g(Qi(e)));var Zw=e=>er(g(Xb(r1(e),()=>Y())));var v1=e=>Ca(j(Bp(e),Zw)),O1=e=>Ca(j(Bp(e),t=>Vw(Xx(t))));var _1=e=>Ce((t,r)=>xY({scope:r,readable:e.evaluate(),onError:e.onError,releaseLockOnEnd:e.releaseLockOnEnd}));var xY=e=>{let t=e.readable.getReader(),r=e.exit??Yu(void 0),n=$(()=>r.current?r.current:zo(Zc({try:()=>t.read(),catch:e.onError}),{onFailure:o=>r.current??ze(o),onSuccess:({done:o,value:s})=>r.current?r.current:o?Y():g(Ee(s))}));return Tt(Kt(e.scope,e.releaseLockOnEnd?X(()=>t.releaseLock()):Qm(()=>t.cancel().catch($n))),n)},gY=(e,t)=>Ce(Se(function*(r,n){let o=e[Symbol.asyncIterator]();return o.return&&(yield*Kt(n,Qm(()=>o.return()))),I(Zc({try:()=>o.next(),catch:t}),s=>s.done?Y(s.value):g(s.value))})),R1=(e,t)=>yo(gY(e,t),Ee),yo=l(2,(e,t)=>Nn(e,r=>X(()=>{let n=0;return j(r,o=>t(o,n++))})));var P1=l(2,(e,t)=>Nn(e,r=>g(He(r,n=>I(t(n),Y))))),M1=e=>e===void 0||e!==\"unbounded\"&&e<=1,lg=l(e=>cr(e[0]),(e,t,r)=>M1(r?.concurrency)?F1(e,t):yY(e,t,r)),F1=(e,t)=>Ce((r,n)=>{let o=0;return j(he(e)(r,n),I(s=>t(s,o++)))}),yY=(e,t,r)=>go(Se(function*(n,o,s){let a=0,i=yield*he(e)(n,o),c=r.concurrency===\"unbounded\"?Number.MAX_SAFE_INTEGER:r.concurrency,u=yield*jp(0);yield*Kt(s,Fn(u));let f=Rn(yield*hi()),p=Ia(s);if(r.unordered){let d=ig(c),m=je(d.release(1)),x=zo({onFailure:y=>I(Bt(u,y),m),onSuccess:y=>I(Sn(u,y),m)});yield*d.take(1).pipe(I(()=>i),I(y=>{let S=a++;return p(f(x($(()=>t(y,S))))),Q}),$t({disableYield:!0}),rt(y=>d.withPermits(c-1)(Bt(u,y))),Ut(s))}else{let d=yield*jp(c-2);yield*Kt(s,Fn(d)),yield*Ms(d).pipe(qb,I(y=>Sn(u,y)),$t({disableYield:!0}),rt(y=>Bt(u,y)),Ut(s));let m,x=y=>{y._tag!==\"Success\"&&(m=y.cause,wa(u,y.cause))};yield*i.pipe(I(y=>{if(m)return ze(m);let S=a++,w=f($(()=>t(y,S)));return p(w),w.addObserver(x),Sn(d,Ea(w))}),$t({disableYield:!0}),rt(y=>Sn(d,sn(y)).pipe(_r(Bt(d,y)))),Ut(s))}return Ms(u)}));var Yw=l(e=>cr(e[0]),(e,t,r)=>M1(r?.concurrency)?SY(e,t):bY(e,t,r)),SY=(e,t)=>Ce((r,n)=>j(he(e)(r,n),o=>{let s,a,i=I(o,u=>(a??=Nt(n),st(he(t(u))(r,a),f=>(s=c(f),s)))),c=He(u=>{if(s=void 0,a.state._tag===\"Empty\")return i;let f=Le(a,xt);return a=void 0,I(f,()=>i)});return $(()=>s??i)})),bY=(e,t,r)=>e.pipe(yo(t),mg(r)),N1=l(2,(e,t)=>Ce((r,n)=>X(()=>{let o,s=Nt(n),a=I(he(e)(r,s),i=>(o=He(i,c=>Le(s,xt).pipe(I(()=>he(t(c))(r,n)),I(u=>(o=u,u)))),o));return $(()=>o??a)})));var Qw=l(4,(e,t,r,n)=>Ce(Se(function*(o,s){let a=yield*he(e)(o,s),i=yield*he(t)(o,s),c=r();return $(()=>{let u=n(c,a,i);return j(u,([f,p])=>(c=p,f))})}))),U1=l(2,(e,t)=>Ce((r,n)=>X(()=>{let o,s=Nt(n),a=I(he(e)(r,s),i=>{let c=i.pipe(Rr(()=>(o=i,Q)),He(u=>Le(s,vr(u)).pipe(_r(he(t(u))(r,n)),I(f=>(o=f,f)))));return o=c,c});return $(()=>o??a)}))),D1=e=>Yw(e,R),gr=e=>Nn(e,t=>{let r,n=0,o=$(function s(){if(r===void 0)return I(t,i=>{switch(i.length){case 0:return s();case 1:return g(i[0]);default:return r=i,g(i[n++])}});let a=r[n++];return n>=r.length&&(r=void 0,n=0),g(a)});return g(o)}),L1=e=>F1(e,Ww),$1=e=>Nn(e,t=>g($t(t,{disableYield:!0}))),B1=l(2,(e,t)=>Kn(typeof t==\"function\"?t(R):t).pipe(j(r=>{let n=Gt.defaultValue();return j1(yg(e,Gt,X(()=>n)),o=>r(o).pipe(j(s=>{n=s}),He(()=>Y(o))))}),Ca)),j1=(e,t)=>Ce((r,n)=>X(()=>{let o,s=()=>{let a=Nt(n);return I(he(e)(r,a),i=>(o=He(i,c=>Le(a,xt).pipe(I(()=>t(c)),I(s))),o))};return $(()=>o??s())})),z1=e=>j1(e,()=>Q),q1=l(2,(e,t)=>Nn(e,(r,n)=>j(Cl(t),o=>Rr(r,o)))),H1=l(2,(e,t)=>Ce((r,n)=>j(he(e)(r,n),o=>I(o,function s(a){return t(a)?g(a):I(o,s)}))));var W1=l(2,(e,t)=>Nn(e,r=>g(I(r,function n(o){let s=[];for(let a=0;aNn(e,r=>g(I(r,function n(o){let s=[];for(let a=0;aNn(e,r=>{let n=I(r,o=>Gb(o,t));return g(I(n,function o(s){return tt(s)?g(s):I(n,o)}))})),G1=l(2,(e,t)=>Nn(e,r=>g(I(r,function n(o){return I(Vb(o,t),s=>tt(s)?g(s):I(r,n))})))),ef=l(e=>cr(e[0]),(e,t,r,n)=>Ce((o,s)=>j(he(e)(o,s),a=>{let i=t(),c,u=0,f,p=zo(a,{onFailure(m){f=m;let x=n?.onHalt&&n.onHalt(i);return x&&x.length>0?g([i,x]):ze(m)},onSuccess(m){let x=r(i,m);return gA(x)?g(x):x}});return $(function m(){if(c===void 0)return f?ze(f):I(p,([y,S])=>(i=y,S.length===0?m():S.length===1?g(S[0]):(c=S,m())));let x=c[u++];return u>=c.length&&(c=void 0,u=0),g(x)})})));var V1=l(3,(e,t,r)=>Ce((n,o)=>j(he(e)(n,o),s=>{let a=t,i=!0;return $(()=>i?(i=!1,g(a)):j(I(s,c=>r(a,c)),c=>(a=c,a)))}))),tf=l(2,(e,t)=>Ce((r,n)=>{let o=Nt(n);return j(he(e)(r,o),s=>{let a=s.pipe(rt(i=>{if(Bo(i))return ze(i);let c=o;return o=Nt(n),Le(c,sn(i)).pipe(_r(he(t(i))(r,o)),I(u=>(a=u,u)))}));return $(()=>a)})})),Z1=l(2,(e,t)=>dg(e,dl,t)),pg=l(2,(e,t)=>tf(e,r=>A1(I(t(r),n=>ze(r))))),Y1=l(3,(e,t,r)=>tf(e,n=>t(n)?r(n):qp(n))),dg=l(3,(e,t,r)=>tf(e,n=>{let o=t(n);return ce(o)?qp(o.failure):r(o.success,n)})),xo=l(2,(e,t)=>dg(e,ab,r=>t(r)));var Q1=l(2,(e,t)=>Nn(e,r=>g(rh(r,n=>qc(n)?Q:ia(t(n)))))),X1=l(e=>cr(e[0]),(e,t,r,n)=>xo(e,o=>t(o)?r(o):n?n(o):es(o))),e2=l(e=>cr(e[0]),(e,t,r,n)=>xo(e,o=>{let s=t(o);return ce(s)?n?n(s.failure):es(s.failure):r(s.success)}));var t2=l(e=>cr(e[0]),(e,t,r,n,o)=>xo(e,s=>{if(At(s,t)&&k(s,\"reason\")){let a=s.reason;return At(a,r)?n(a,s):o?o(a,s):es(s)}return es(s)})),r2=l(e=>cr(e[0]),(e,t,r,n)=>{let o;return xo(e,s=>{if(At(s,t)&&k(s,\"reason\")&&k(s.reason,\"_tag\")&&Hm(s.reason._tag)){let a=s.reason;return o??=new Set(Object.keys(r)),o.has(a._tag)?r[a._tag](a,s):n?n(a,s):es(s)}return es(s)})});var n2=l(2,(e,t)=>xo(e,r=>es(t(r)))),o2=e=>xo(e,Gw),s2=l(e=>cr(e[0]),(e,t)=>{if(!t?.log)return xo(e,()=>Xu);let r=ih(t.log===!0?void 0:t.log);return xo(pg(e,n=>sb(n)?r(n):Q),()=>Xu)}),x1=e=>tf(e,()=>Xu),a2=l(e=>cr(e[0]),(e,t)=>{if(!t?.log)return x1(e);let r=ih(t.log===!0?void 0:t.log);return x1(pg(e,n=>r(n)))}),i2=l(2,(e,t)=>cg(()=>{let r,n=Gt.defaultValue(),o=yg(e,Gt,X(()=>n)),s=r0(o,()=>(r=void 0,Q)),a=typeof t==\"function\"?t(R):t,i=xo(s,Se(function*(c){return r||(r=yield*Kn(a)),n=yield*r(c),i},(c,u)=>He(c,()=>g(es(u))),Ca));return i})),c2=l(e=>cr(e[0]),(e,t,r)=>e.pipe(yo(t),mg({...r,concurrency:r?.concurrency??1,switch:!0}))),mg=l(2,(e,{bufferSize:t=16,concurrency:r,switch:n=!1})=>go(Se(function*(o,s,a){let i=r===\"unbounded\"?Number.MAX_SAFE_INTEGER:Math.max(1,r),c=n?void 0:ig(i),u=yield*GD(!0),f=new Set,p=yield*jp(t);yield*Kt(a,Fn(p));let d=yield*he(e)(o,s);return yield*dr(function*(){for(;;){let m;c&&(f.size=i;){let E=lA(f);f.delete(E),f.size===0&&(yield*u.open),yield*mo(E)}let w=yield*S.pipe(Rr(()=>Rl),I(E=>Sn(p,E)),$t({disableYield:!0}),Vn(Se(function*(E){let U=Jm(E),_=yield*vn(Le(y,U));if(!f.has(w))return;let h=hb([U,_]);Uo(h)&&(yield*Bt(p,h.cause)),f.delete(w),c&&(yield*c.release(1)),f.size===0&&(yield*u.open)})),eu);u.closeUnsafe(),f.add(w)}}).pipe(rt(m=>{let x=Tl(m);return mt(x)?u.whenOpen(Bt(p,m)):Bt(p,m)}),Ut(a)),Ms(p)}))),hg=l(e=>cr(e[0])&&cr(e[1]),(e,t,r)=>go(Se(function*(n,o,s){let a=r?.haltStrategy??\"both\",i=yield*jp(0);yield*Kt(s,Fn(i));let c=0;function u(p,d){if(c++,!Bo(d))return Bt(i,d);switch(a){case\"both\":return c===2?Bt(i,d):Q;case\"left\":case\"right\":return p===a?Bt(i,d):Q;case\"either\":return Bt(i,d)}}let f=(p,d,m)=>he(d)(n,m).pipe(I(x=>x.pipe(I(y=>Sn(i,y)),$t)),Vn(x=>Fl(Jm(x),y=>Le(m,y)).pipe(zo({onFailure:y=>u(p,y),onSuccess:()=>u(p,x)}))),Ut(s));return yield*f(\"left\",e,Nt(s)),yield*f(\"right\",t,Nt(s)),Ms(i)}))),u2=l(2,(e,t)=>hg(e,A1(t),{haltStrategy:\"left\"})),f2=()=>Ce((e,t)=>X(()=>{let r=\"\",n=!1,o=v();function s(c){let u=[];function f(p){r.length===0?u.push(p):(u.push(r+p),r=\"\")}for(let p=0;po._tag===\"Some\"?Y(o.value):Km(e,{onSuccess:i,onFailure:ze,onDone:c=>{if(o=O(c),r.length>0){let u=r;return r=\"\",n=!1,g([u])}return Y(c)}}));function i(c){let u=s(c);return u!==null?g(u):a}return a}));var Xw=l(2,(e,t)=>Ce((r,n)=>I(he(e)(r,n),o=>he(t)(o,n)))),e0=l(2,(e,t)=>Ce((r,n)=>I(he(e)(r,n),o=>{let s=rt(o,a=>Bo(a)?ze(a):an(ub(a)));return j(he(t)(s,n),a=>Wb(a,i=>qc(i)?ze(i.value):an(i)))}))),Ca=e=>Ce((t,r)=>{let n;return g($(()=>n||e.pipe(Es(r),I(o=>he(o)(t,r)),I(o=>n=o))))}),l2=e=>go((t,r,n)=>j(Es(he(e)(t,r),n),Es(n)));var p2=l(2,(e,t)=>Ce(Se(function*(r,n){let o=yield*he(e)(r,n),s=yield*Mn({capacity:t.capacity===\"unbounded\"?void 0:t.capacity,strategy:t.capacity===\"unbounded\"?void 0:t.strategy});return yield*Kt(n,Fn(s)),yield*o.pipe(I(a=>Sn(s,a)),$t({disableYield:!0}),Vn(a=>Bt(s,a)),Ut(n)),Ms(s)}))),d2=l(2,(e,t)=>Ce(Se(function*(r,n){let o=yield*he(e)(r,n),s=yield*Mn({capacity:t.capacity===\"unbounded\"?void 0:t.capacity,strategy:t.capacity===\"unbounded\"?void 0:t.strategy});return yield*Kt(n,Fn(s)),yield*o.pipe(I(a=>Yi(s,a)),$t({disableYield:!0}),Vn(a=>Bt(s,a)),Ut(n)),Qi(s)}))),t0=l(2,(e,t)=>hg(e,er(g(I(t,Y))),{haltStrategy:\"either\"})),m2=l(2,(e,t)=>go(Se(function*(r,n,o){let s=yield*he(e)(r,n),a=yield*Ut(t,o,{startImmediately:!0});return $(()=>{let i=a.pollUnsafe();return i===void 0?s:mb(i,{onFailure:ze,onSuccess:Y})})}))),h2=l(2,(e,t)=>Hp(e,r=>Uo(r)?t(r.cause):Q)),Hp=l(2,(e,t)=>go((r,n,o)=>Lo(o,t).pipe(_r(he(e)(r,n))))),x2=l(2,(e,t)=>Ca(Tt(t,e))),r0=l(2,(e,t)=>Nn(e,r=>X(()=>{let n=!0,o=Rr(r,s=>(n=!1,t(s)));return $(()=>n?o:r)}))),g2=l(2,(e,t)=>Nn(e,r=>g(He(r,n=>I(t,()=>Y(n)))))),y2=l(2,(e,t)=>Hp(e,r=>t)),Xi=(e,t,r)=>$(()=>{let n=kn(),o=he(e)(Y(),n);return He(I(o,t),r||g).pipe(un(s=>Le(n,s)))});var xg=l(2,(e,t)=>Ce((r,n)=>j(On(he(e)(r,n),t),On(t)))),gg=l(3,(e,t,r)=>Ce((n,o)=>j(qo(he(e)(n,o),t,r),qo(t,r)))),yg=l(3,(e,t,r)=>Ce((n,o)=>I(r,s=>he(gg(e,t,s))(n,o)))),S2=l(e=>cr(e[0]),(e,t,r)=>Uf(t)?xg(e,t):Ce((n,o)=>I(r?.local?yl(t,gl(),o):Lm(t,o),s=>j(On(he(e)(n,o),s),On(s))))),b2=l(2,(e,t)=>Ce((r,n)=>Ml(o=>{let s=t(o);return he(xg(e,s))(r,n)})));var n0=function(){let e=cr(arguments[0]),t=e?arguments[1]:arguments[0],r=Gs(e?arguments[2]:arguments[1]);if(e){let n=arguments[0];return g1(n,t,r)}return n=>g1(n,t,r)},g1=(e,t,r)=>hY(eE(t,r),n=>gg(e,ei,n),(n,o)=>cn(s=>{let a=s.getRef(Nr),i=s.getRef(ZO);return Rm(n,o,a,i)}));var E2=e=>Xi(e,t=>$t(t,{disableYield:!0})),ec=l(2,(e,t)=>Xi(e,r=>$t(I(r,t),{disableYield:!0}))),I2=l(2,(e,t)=>Xi(e,r=>r.pipe(I(t),I(n=>n?Q:Y()),$t({disableYield:!0})))),o0=e=>j(tc(e,()=>({bytes:0,arrays:[]}),(t,r)=>{for(let n=0;n{let n=new Uint8Array(r),o=0;for(let s=0;s$(()=>{let t=v();return Xi(e,r=>r.pipe(eh,I(n=>(t=n,Y()))),()=>g(t))}),w2=e=>$(()=>{let t=Symbol(),r=t;return Xi(e,n=>$t(I(n,o=>(r=o,Q)),{disableYield:!0}),()=>r===t?aa:zb(r))}),tc=l(3,(e,t,r)=>$(()=>{let n=t();return Xi(e,o=>Gn({while:Sr,body:()=>o,step:s=>{n=r(n,s)}}),()=>g(n))})),C2=l(3,(e,t,r)=>$(()=>{let n=t();return Xi(e,o=>Gn({while:Sr,body:je(o.pipe(I(s=>r(n,s)),j(s=>{n=s}))),step:$n}),()=>g(n))})),s0=Se(function*(e){let t=ig(1),r=yield*hi(),n=Fe(r,Or);return(yield*he(e)(Y(),n)).pipe(On(r),t.withPermits(1))},rt(e=>g(ze(e)))),ts=(e,t)=>he(e)(Y(),t);var a0=l(e=>cr(e[0]),(e,t)=>Ho(r=>ec(e,n=>Yi(t,n)).pipe(r,vn,I(n=>(Ve(n)?og(t):wa(t,n.cause),Q)))));var A2=l(e=>cr(e[0]),Se(function*(e,t){let r=yield*xi,n=yield*Mn({capacity:typeof t.capacity==\"number\"?t.capacity:void 0,strategy:typeof t.capacity==\"number\"?t.strategy:void 0});return yield*Kt(r,Fn(n)),yield*Ut(a0(e,n),r),n}));var k2=e=>gi(e.capacity===\"unbounded\"?Zx(e):e.strategy===\"dropping\"?Gx(e):e.strategy===\"sliding\"?Vx(e):Kx(e),$p),v2=l(2,Se(function*(e,t){let r=yield*k2(t);return yield*tu(i0(e,r,{shutdownOnEnd:t.shutdownOnEnd!==!1})),r})),i0=l(e=>cr(e[0]),(e,t,r)=>ec(e,n=>t1(t,n)).pipe(r?.shutdownOnEnd===!0?yi($p(t)):R)),O2=l(2,Se(function*(e,t){let r=yield*k2(t);return yield*ec(e,n=>Qx(r,n)).pipe(un(n=>Yx(r,n)),tu),r}));var Mr={};Eo(Mr,{Array:()=>DX,Boolean:()=>$X,ByteSize:()=>jX,ConfigError:()=>af,Date:()=>JX,Duration:()=>BX,Finite:()=>MX,Int:()=>FX,Literal:()=>NX,Literals:()=>UX,LogLevel:()=>qX,NonEmptyString:()=>RX,Number:()=>PX,Port:()=>zX,Record:()=>LX,Redacted:()=>HX,String:()=>_X,URL:()=>WX,all:()=>XL,fail:()=>vX,flatMap:()=>SX,isConfig:()=>YL,map:()=>QL,mapEffect:()=>bX,nested:()=>KX,option:()=>IX,orElse:()=>EX,schema:()=>tr,succeed:()=>OX,unwrap:()=>t$,withDefault:()=>e$});var sf={};Eo(sf,{ConfigProvider:()=>ac,SourceError:()=>Tg,constantCase:()=>rX,fromDir:()=>gX,fromDotEnv:()=>xX,fromDotEnvContents:()=>JL,fromEnv:()=>WL,fromEnvRecord:()=>R0,fromUnknown:()=>aX,layer:()=>oX,layerAdd:()=>sX,make:()=>Cg,makeArray:()=>O0,makeRecord:()=>wg,makeValue:()=>Yp,mapInput:()=>_0,nested:()=>nX,orElse:()=>v0});var _2=\"~effect/PlatformError\",rf=class extends Fr(\"BadArgument\"){get message(){return`${this.module}.${this.method}${this.description?`: ${this.description}`:\"\"}`}};var R2=class extends Fr(\"PlatformError\"){constructor(t){\"cause\"in t?super({reason:t,cause:t.cause}):super({reason:t})}[_2]=_2;get message(){return this.reason.message}};var IY=\"~effect/Stream\",TY={_R:R,_E:R,_A:R},P2=function(e){this.channel=e};P2.prototype={[IY]:TY,pipe(){return Z(this,arguments)}};var M2=e=>new P2(e);var CY=\"~effect/Sink\";var AY={_A:R,_In:R,_L:R,_E:R,_R:R},kY={[CY]:AY,pipe(){return Z(this,arguments)}};var vY=e=>{let t=Object.create(kY);return t.transform=e,t},F2=e=>Ce((t,r)=>g(I(e.transform(t,r),Y)));var N2=e=>{let t=ot(e);return vY(r=>{let n=[];if(t===0)return g([n]);let o;return r.pipe(I(s=>{if(n.length+s.length<=t)return n.push(...s),n.length===t?Y():Q;for(let a=0;ag([n,o])))})};var ns={};Eo(ns,{DefaultChunkSize:()=>V2,Do:()=>y7,TypeId:()=>G2,accumulate:()=>t7,aggregate:()=>K9,aggregateWithin:()=>b0,as:()=>uQ,bind:()=>b7,bindEffect:()=>E7,bindTo:()=>I7,broadcast:()=>TL,broadcastN:()=>G9,buffer:()=>e9,bufferArray:()=>t9,callback:()=>Q2,catch:()=>Zp,catchCause:()=>uL,catchCauseFilter:()=>l9,catchCauseIf:()=>f9,catchDefect:()=>r9,catchFilter:()=>y0,catchIf:()=>lL,catchReason:()=>i9,catchReasons:()=>c9,catchTag:()=>s9,catchTags:()=>a9,changes:()=>r7,changesWith:()=>wL,changesWithEffect:()=>n7,chunks:()=>yL,collect:()=>e7,combine:()=>_9,combineArray:()=>R9,concat:()=>nc,cross:()=>MQ,crossWith:()=>iL,debounce:()=>D9,decodeText:()=>o7,die:()=>HY,drain:()=>yQ,drainFork:()=>SQ,drop:()=>S0,dropRight:()=>k9,dropUntil:()=>w9,dropUntilEffect:()=>C9,dropWhile:()=>xL,dropWhileEffect:()=>gL,dropWhileFilter:()=>A9,empty:()=>Ns,encodeText:()=>s7,ensuring:()=>d7,fail:()=>nf,failCause:()=>X2,failCauseSync:()=>WY,failSync:()=>qY,filter:()=>JQ,filterEffect:()=>GQ,filterMap:()=>KQ,filterMapEffect:()=>VQ,flatMap:()=>of,flatten:()=>oL,flattenArray:()=>gQ,flattenEffect:()=>lQ,flattenIterable:()=>CQ,flattenTake:()=>AQ,forever:()=>wQ,fromArray:()=>Gp,fromArrayEffect:()=>VY,fromArrays:()=>ZY,fromAsyncIterable:()=>eQ,fromChannel:()=>A,fromEffect:()=>Sg,fromEffectDrain:()=>Z2,fromEffectRepeat:()=>Y2,fromEffectSchedule:()=>$Y,fromEventListener:()=>nQ,fromIterable:()=>oc,fromIterableEffect:()=>KY,fromIterableEffectRepeat:()=>GY,fromIteratorSucceed:()=>JY,fromPubSub:()=>QY,fromPubSubTake:()=>eL,fromPull:()=>rs,fromQueue:()=>YY,fromReadableStream:()=>XY,fromSchedule:()=>tQ,fromSubscription:()=>rQ,groupAdjacentBy:()=>W9,groupBy:()=>q9,groupByKey:()=>H9,grouped:()=>j9,groupedWithin:()=>z9,haltWhen:()=>f7,ignore:()=>h9,ignoreCause:()=>x9,interleave:()=>c7,interleaveWith:()=>AL,interruptWhen:()=>u7,intersperse:()=>CL,intersperseAffixes:()=>i7,isStream:()=>Ie,iterate:()=>sQ,let:()=>S7,limitBytes:()=>b9,make:()=>jY,map:()=>Jr,mapAccum:()=>P9,mapAccumArray:()=>sc,mapAccumArrayEffect:()=>F9,mapAccumEffect:()=>M9,mapArray:()=>rL,mapArrayEffect:()=>pQ,mapBoth:()=>fQ,mapEffect:()=>Vp,mapError:()=>pL,merge:()=>d0,mergeAll:()=>PQ,mergeEffect:()=>bg,mergeLeft:()=>_Q,mergeResult:()=>OQ,mergeRight:()=>RQ,mkArrayBuffer:()=>F7,mkString:()=>M7,mkUint8Array:()=>N7,never:()=>iQ,onEnd:()=>p7,onError:()=>l7,onExit:()=>kL,onFirst:()=>OL,onStart:()=>vL,orDie:()=>m9,orElseIfEmpty:()=>p9,orElseSucceed:()=>d9,paginate:()=>oQ,partition:()=>YQ,partitionEffect:()=>ZQ,partitionQueue:()=>x0,peel:()=>XQ,pipeThrough:()=>X9,pipeThroughChannel:()=>Y9,pipeThroughChannelOrFail:()=>Q9,prepend:()=>vQ,provide:()=>_L,provideContext:()=>m7,provideService:()=>h7,provideServiceEffect:()=>E0,race:()=>WQ,raceAll:()=>cL,range:()=>aQ,rechunk:()=>SL,repeat:()=>bQ,repeatElements:()=>TQ,result:()=>dQ,retry:()=>g9,run:()=>T7,runCollect:()=>PL,runCount:()=>w7,runDrain:()=>Ig,runFold:()=>A7,runFoldEffect:()=>k7,runForEach:()=>_7,runForEachArray:()=>ML,runForEachWhile:()=>R7,runHead:()=>v7,runIntoPubSub:()=>B7,runIntoQueue:()=>q7,runLast:()=>O7,runSum:()=>C7,scan:()=>N9,scanEffect:()=>U9,schedule:()=>EQ,scoped:()=>cQ,service:()=>DY,serviceOption:()=>LY,share:()=>Z9,sliding:()=>v9,slidingSize:()=>bL,split:()=>O9,splitLines:()=>a7,succeed:()=>rc,suspend:()=>Wr,switchMap:()=>xQ,sync:()=>zY,take:()=>S9,takeRight:()=>E9,takeUntil:()=>dL,takeUntilEffect:()=>mL,takeWhile:()=>hL,takeWhileEffect:()=>T9,takeWhileFilter:()=>I9,tap:()=>nL,tapBoth:()=>mQ,tapCause:()=>fL,tapDefect:()=>o9,tapError:()=>g0,tapErrorTag:()=>n9,tapSink:()=>hQ,throttle:()=>B9,throttleEffect:()=>EL,tick:()=>BY,timeout:()=>IQ,timeoutOrElse:()=>sL,toAsyncIterable:()=>$7,toAsyncIterableEffect:()=>L7,toAsyncIterableWith:()=>T0,toChannel:()=>Un,toPubSub:()=>j7,toPubSubTake:()=>FL,toPull:()=>P7,toQueue:()=>z7,toReadableStream:()=>U7,toReadableStreamEffect:()=>D7,toReadableStreamWith:()=>I0,transduce:()=>J9,transformPull:()=>Dt,transformPullBracket:()=>p0,unfold:()=>tL,unwrap:()=>Aa,unwrapReason:()=>u9,updateContext:()=>RL,updateService:()=>x7,when:()=>QQ,withExecutionPlan:()=>y9,withSpan:()=>g7,zip:()=>NQ,zipFlatten:()=>LQ,zipLatest:()=>qQ,zipLatestAll:()=>h0,zipLatestWith:()=>HQ,zipLeft:()=>UQ,zipRight:()=>DQ,zipWith:()=>m0,zipWithArray:()=>Eg,zipWithIndex:()=>$Q,zipWithNext:()=>BQ,zipWithPrevious:()=>jQ,zipWithPreviousAndNext:()=>zQ});var D2=\"~effect/RcMap\",OY=e=>({[D2]:D2,lookup:e.lookup,context:e.context,scope:e.scope,idleTimeToLive:e.idleTimeToLive,capacity:e.capacity,state:{_tag:\"Open\",map:Up()},pipe(){return Z(this,arguments)}}),L2=e=>cn(t=>{let r=t.context,n=Fe(r,Or),o=OY({lookup:e.lookup,context:r,scope:n,idleTimeToLive:typeof e.idleTimeToLive==\"function\"?rC(e.idleTimeToLive,Ge):je(Ge(e.idleTimeToLive??Ws)),capacity:Math.max(e.capacity??Number.POSITIVE_INFINITY,0)});return Tt(Lo(n,()=>{if(o.state._tag===\"Closed\")return Q;let s=o.state.map;return o.state={_tag:\"Closed\"},sa(s,([,a])=>vn(Wp(a))).pipe(Rr(()=>X(()=>{Ux(s)})))}),o)}),u0=l(2,(e,t)=>Ho(r=>{if(e.state._tag===\"Closed\")return _n;let n=e.state,o=Zu(),s=Xo(n.map,t),a;if(s._tag===\"Some\")a=s.value,a.refCount++;else{if(Number.isFinite(e.capacity)&&WD(e.state.map)>=e.capacity)return P(new fb(`RcMap attempted to exceed capacity of ${e.capacity}`));{a={deferred:ui(),scope:kn(),idleTimeToLive:e.idleTimeToLive(t),finalizer:void 0,fiber:void 0,expiresAt:0,refCount:1},a.finalizer=RY(e,t,a),Dp(n.map,t,a);let c=new Map(e.context.mapUnsafe);o.context.mapUnsafe.forEach((f,p)=>{c.set(p,f)}),c.set(Or.key,a.scope);let u=Oo(c);$(()=>e.lookup(t)).pipe(Rn(u),Ia(a.scope)).addObserver(f=>{db(f)&&_Y(e,t,a,u),Do(a.deferred,f)})}}let i=_o(o.context,Or);return Kt(i,a.finalizer).pipe(_r(r(Fm(a.deferred))))}));var _Y=(e,t,r,n)=>{if(e.state._tag===\"Closed\")return;let o=Xo(e.state.map,t);o._tag===\"None\"||o.value!==r||(Vu(e.state.map,t),!(r.refCount>0)&&Wp(r).pipe(Rn(n),Ia(e.scope)))},Wp=e=>e.fiber?mo(e.fiber).pipe(_r(Le(e.scope,xt))):Le(e.scope,xt),RY=(e,t,r)=>cn(n=>{if(r.refCount--,r.refCount>0)return Q;if(e.state._tag===\"Closed\")return Wp(r);let o=Xo(e.state.map,t);if(o._tag===\"None\"||o.value!==r)return Wp(r);if(vc(r.idleTimeToLive))return Vu(e.state.map,t),Wp(r);if(!Ks(r.idleTimeToLive))return Q;let s=n.getRef(Hc);return r.expiresAt=s.currentTimeMillisUnsafe()+Cr(r.idleTimeToLive),r.fiber?Q:(r.fiber=Ho(function a(i){let c=s.currentTimeMillisUnsafe(),u=r.expiresAt-c;if(u<=0){if(e.state._tag===\"Closed\"||r.refCount>0)return Q;let f=Xo(e.state.map,t);return f._tag===\"None\"||f.value!==r?Q:(Vu(e.state.map,t),Le(r.scope,xt))}return I(i(s.sleep(Js(u))),()=>a(i))}).pipe(yi(X(()=>{r.fiber=void 0})),Rn(n.context),Ia(e.scope)),Q)});var f0=l(2,(e,t)=>Si(r=>{if(e.state._tag===\"Closed\")return Q;let n=Xo(e.state.map,t);if(n._tag===\"None\"||vc(n.value.idleTimeToLive))return Q;let o=n.value;return o.expiresAt=r.currentTimeMillisUnsafe()+Cr(o.idleTimeToLive),Q}));var PY=\"~effect/RcRef\",Jp={_tag:\"Empty\"},MY={_tag:\"Closed\"},FY={_A:R,_E:R},l0=class{[PY]=FY;pipe(){return Z(this,arguments)}state=Jp;acquire;context;scope;idleTimeToLive;constructor(t,r,n,o){this.acquire=t,this.context=r,this.scope=n,this.idleTimeToLive=o}},j2=e=>cn(t=>{let r=t.context,n=Fe(r,Or),o=new l0(e.acquire,r,n,e.idleTimeToLive!==void 0?Ge(e.idleTimeToLive):void 0);return Tt(Lo(n,()=>{let s=o.state;return o.state=MY,s._tag===\"Acquired\"?Le(s.scope,xt):s._tag===\"Acquiring\"?mo(s.fiber):Q}),o)}),NY=(e,t)=>{switch(e.state._tag){case\"Closed\":return _n;case\"Acquired\":{let r=e.state;return r.refCount++,r.fiber?Tt(mo(r.fiber),r):g(r)}case\"Acquiring\":{let r=e.state;return r.awaiters++,B2(e,r,t)}case\"Empty\":{let r={_tag:\"Acquiring\",fiber:void 0,awaiters:1,acquired:void 0};e.state=r;let n=kn(),o=On(e.acquire,Jt(e.context,Or,n)).pipe(I(s=>{if(e.state!==r)return _n;let a={_tag:\"Acquired\",value:s,scope:n,fiber:void 0,refCount:r.awaiters,invalidated:!1};return e.state=a,r.acquired=a,g(a)}),un(s=>Ve(s)?Q:(e.state===r&&(e.state=Jp),Le(n,s))));return r.fiber=Rn(Zu().context)(o),B2(e,r,t)}}},B2=(e,t,r)=>un(r(Ea(t.fiber)),n=>{let o=t.acquired;return o!==void 0?Uo(n)?q2(e,o):Q:--t.awaiters>0||e.state!==t?Q:(e.state=Jp,mo(t.fiber))}),z2=e=>{let t=e;return Ho(r=>I(xi,n=>I(NY(t,r),o=>Tt(Lo(n,()=>q2(t,o)),o.value))))},q2=(e,t)=>{if(t.refCount--,t.refCount>0)return Q;let r=e.idleTimeToLive;return r===void 0||t.invalidated?(e.state===t&&(e.state=Jp),Le(t.scope,xt)):Ks(r)?(t.fiber=Ho(n=>I(n(ua(r)),()=>e.state===t&&t.refCount===0?(e.state=Jp,Le(t.scope,xt)):Q)).pipe(yi(X(()=>{t.fiber=void 0})),Rn(e.context),Ia(e.scope)),Q):Q};var H2=j2,W2=z2;var G2=\"~effect/Stream\",Ie=e=>k(e,G2),V2=zp,A=M2,Sg=e=>A(fg(j(e,Ee))),DY=e=>Sg(Yb(e)),LY=e=>Sg(Qb(e)),Z2=e=>rs(g(I(e,()=>Y()))),Y2=e=>rs(g(j(e,Ee))),$Y=(e,t)=>rs(dr(function*(){let r=yield*Kn(t),n=yield*qo(e,Gt,Gt.defaultValue()),o=!0,s=$(()=>r(n)).pipe(I(a=>qo(e,Gt,a)),j(a=>(n=a,Ee(a))));return $(()=>o?(o=!1,g(Ee(n))):s)})),BY=e=>rs(X(()=>{let t=!0,r=g(Ee(void 0)),n=Jb(r,e);return $(()=>t?(t=!1,r):n)})),rs=e=>A(er(e)),Dt=(e,t)=>A(Ce((r,n)=>I(ts(e.channel,n),o=>t(o,n)))),p0=(e,t)=>A(go((r,n,o)=>I(ts(e.channel,n),s=>t(s,n,o)))),Un=e=>e.channel,Q2=(e,t)=>A(S1(e,t)),Ns=A(Xu),rc=e=>A(ug(Ee(e))),jY=(...e)=>Gp(e),zY=e=>A(I1(()=>Ee(e()))),Wr=e=>A(cg(()=>e().channel)),nf=e=>A(es(e)),qY=e=>A(w1(e)),X2=e=>A(qp(e)),HY=e=>A(Gw(e)),WY=e=>A(C1(e)),JY=(e,t)=>A(Kw(()=>e,t)),oc=(e,t)=>Array.isArray(e)&&t?.chunkSize===void 0?Gp(e):A(E1(e,t?.chunkSize)),KY=e=>Aa(j(e,oc)),GY=e=>of(Y2(e),oc),Gp=e=>tt(e)?A(ug(e)):Ns,VY=e=>Aa(j(e,Gp)),ZY=(...e)=>A(Jw(jd(e,tt))),YY=e=>A(k1(e)),QY=e=>A(v1(e)),eL=e=>A(O1(e)),XY=e=>A(_1(e)),eQ=(e,t)=>A(R1(e,t)),tQ=e=>rs(j(Cl(e),t=>He(j(t(void 0),Ee),()=>Y()))),rQ=e=>A(Zw(e)),nQ=(e,t,r)=>Q2(n=>{let o=typeof r==\"object\"&&r.once;function s(a){rg(n,a),o&&og(n)}return gi(X(()=>e.addEventListener(t,s,r)),()=>X(()=>e.removeEventListener(t,s,r)))},{bufferSize:typeof r==\"object\"?r.bufferSize:void 0}),tL=(e,t)=>rs(X(()=>{let r=e;return I($(()=>t(r)),n=>n===void 0?Y():(r=n[1],g(Ee(n[0]))))})),oQ=(e,t)=>rs(X(()=>{let r=e,n=!1;return $(function o(){return n?Y():I(t(r),([s,a])=>(fe(a)?n=!0:r=a.value,tt(s)?g(s):o()))})})),sQ=(e,t)=>tL(e,r=>g([r,t(r)])),aQ=(e,t,r=zp)=>e>t?Ns:rs(X(()=>{let n=Tn(r),o=e,s=!1;return $(()=>{if(s)return Y();let a=t-o+1;if(a>n){let c=xy(o,o+n-1);return o+=n,g(c)}let i=xy(o,o+a-1);return s=!0,g(i)})})),iQ=A(T1),Aa=e=>A(Ca(j(e,Un))),cQ=e=>A(l2(e.channel)),Jr=l(2,(e,t)=>Wr(()=>{let r=0;return A(yo(e.channel,wn(n=>t(n,r++))))})),uQ=l(2,(e,t)=>Jr(e,()=>t)),fQ=l(2,(e,t)=>e.pipe(Jr(t.onElement),pL(t.onError))),rL=l(2,(e,t)=>A(yo(e.channel,t))),Vp=l(e=>Ie(e[0]),(e,t,r)=>e.channel.pipe(gr,lg(t,r),yo(Ee),A)),lQ=l(e=>Ie(e[0]),(e,t)=>Vp(e,R,t)),pQ=l(2,(e,t)=>A(lg(e.channel,t))),dQ=e=>e.pipe(Jr(B),Zp(t=>rc(W(t)))),nL=l(e=>Ie(e[0]),(e,t,r)=>Vp(e,n=>Tt(t(n),n),r)),mQ=l(2,(e,t)=>e.pipe(g0(t.onError),nL(t.onElement,{concurrency:t.concurrency}))),hQ=l(2,(e,t)=>p0(e,Se(function*(r,n,o){let s=Pr(),a=Pr(),i,c,u=!1,f=!1,p=s.whenOpen($(()=>{if(i){let m=i;return i=void 0,f||s.closeUnsafe(),Tt(a.open,m)}return Y()}));yield*$(()=>t.transform(p,o)).pipe(m=>Fl(m,x=>(u=!0,Uo(x)&&(c=x.cause),a.open),!0),Ut(o));let d=r.pipe(I(m=>c?ze(c):u?g(m):(i=m,a.closeUnsafe(),s.openUnsafe(),Tt(a.await,m))),He(()=>c?ze(c):u?Y():(f=!0,a.closeUnsafe(),s.openUnsafe(),I(a.await,()=>c?ze(c):Y()))));return $(()=>c?ze(c):u?r:d)}))),of=l(e=>Ie(e[0]),(e,t,r)=>e.channel.pipe(gr,Yw(n=>t(n).channel,r),A)),xQ=l(e=>Ie(e[0]),(e,t,r)=>e.channel.pipe(gr,c2(n=>t(n).channel,r),A)),oL=l(e=>Ie(e[0]),(e,t)=>of(e,R,t)),gQ=e=>A(gr(e.channel)),yQ=e=>A($1(e.channel)),SQ=l(2,(e,t)=>bg(e,Ig(t))),bQ=l(2,(e,t)=>A(B1(e.channel,t))),EQ=l(2,(e,t)=>e.channel.pipe(gr,q1(t),yo(Ee),A)),IQ=l(2,(e,t)=>sL(e,{duration:t,orElse:()=>Ns})),sL=l(2,(e,t)=>{let r=Ge(t.duration);if(!Ks(r))return e;if(vc(r))return Wr(t.orElse);let n=Symbol();return uL(Wr(()=>{let s=Zu().getRef(Hc),a=Cr(r),i,c=Pr(!1);return d0(Dt(e,(u,f)=>$(()=>(i=s.currentTimeMillisUnsafe()+a,c.openUnsafe(),u)).pipe(j(p=>(c.closeUnsafe(),i=void 0,p)),g)),Z2(dr(function*(){for(;;)if(yield*c.await,!(i===void 0||(yield*ua(i-s.currentTimeMillisUnsafe()),i===void 0)||i-s.currentTimeMillisUnsafe()>0))return yield*an(n)})),{haltStrategy:\"left\"})}),o=>o.reasons.find(a=>a._tag===\"Die\"&&a.defect===n)?t.orElse():X2(o))}),TQ=l(2,(e,t)=>A(Ce((r,n)=>j(he(gr(e.channel))(r,n),o=>{let s,a=dr(function*(){let i=yield*o,c=Ee(i);return s=(yield*Cl(t))(i).pipe(Tt(c),He(f=>(s=void 0,a))),c});return $(()=>s??a)})))),wQ=e=>A(z1(e.channel)),CQ=e=>of(e,oc),AQ=e=>e.channel.pipe(gr,L1,A),nc=l(2,(e,t)=>{let r=A(D1(b1(()=>kQ(e,t))));return aL.set(r,[e,t]),r}),aL=new WeakMap;function*kQ(e,t){let r=[t,e];for(;r.length>0;){let n=r.pop(),o=aL.get(n);o===void 0?yield n.channel:r.push(o[1],o[0])}}var vQ=l(2,(e,t)=>nc(oc(t),e)),d0=l(e=>Ie(e[0])&&Ie(e[1]),(e,t,r)=>A(hg(Un(e),Un(t),r))),bg=l(2,(e,t)=>e.channel.pipe(u2(t),A)),OQ=l(2,(e,t)=>d0(Jr(e,B),Jr(t,W))),_Q=l(2,(e,t)=>bg(e,Ig(t))),RQ=l(2,(e,t)=>bg(t,Ig(e))),PQ=l(2,(e,t)=>oL(oc(e),t)),MQ=l(2,(e,t)=>iL(e,t,(r,n)=>[r,n])),iL=l(3,(e,t,r)=>of(e,n=>Jr(t,o=>r(n,o)))),m0=l(3,(e,t,r)=>Eg(e,t,FQ(r))),FQ=e=>(t,r)=>{let n=Math.min(t.length,r.length),o=[];for(let s=0;sA(go(Se(function*(n,o){let s=yield*ts(e.channel,o),a=yield*ts(t.channel,o),i=dr(function*(){let f=yield*Ut(s,o),p=yield*Ut(a,o);return yield*KD([f,p])}),c={_tag:\"PullBoth\"};return dr(function*(){let[f,p]=c._tag===\"PullBoth\"?yield*i:c._tag===\"PullLeft\"?[yield*s,c.rightArray]:[c.leftArray,yield*a],d=r(f,p);return tt(d[1])?c={_tag:\"PullRight\",leftArray:d[1]}:tt(d[2])?c={_tag:\"PullLeft\",rightArray:d[2]}:c={_tag:\"PullBoth\"},d[0]})})))),NQ=l(2,(e,t)=>m0(e,t,(r,n)=>[r,n])),UQ=l(2,(e,t)=>Eg(e,t,(r,n)=>{let o=Math.min(r.length,n.length),s=r.slice(0,o),a=r.slice(o),i=n.slice(o);return[s,a,i]})),DQ=l(2,(e,t)=>Eg(e,t,(r,n)=>{let o=Math.min(r.length,n.length),s=n.slice(0,o),a=r.slice(o),i=n.slice(o);return[s,a,i]})),LQ=l(2,(e,t)=>m0(e,t,(r,n)=>[...r,n])),$Q=e=>Jr(e,(t,r)=>[t,r]),BQ=e=>sc(e,v,(t,r)=>{let n=0;t._tag===\"None\"&&(n=1,t=O(r[0]));let o=Pt();for(;nsc(e,v,(t,r)=>{let n=Pt();for(let o=0;osc(e,()=>({prev:v(),current:v()}),(t,r)=>{let n=0,o;t.current._tag===\"None\"?(n=1,o=r[0],t.current=O(o)):o=t.current.value;let s=Pt();for(;nA(cg(()=>{let t=[],r=new Set,n=Pr(),o=Pr();return mg(Jw(e.map((s,a)=>s.channel.pipe(gr,lg(i=>(t[a]=i,r.has(a)?g(Ee(t.slice())):(r.add(a),r.sizer.has(a)?Q:o.open)))),{concurrency:\"unbounded\",bufferSize:0}).pipe(t0(o.await))})),qQ=l(2,(e,t)=>h0(e,t)),HQ=l(3,(e,t,r)=>Jr(h0(e,t),([n,o])=>r(n,o))),cL=(...e)=>A(Ce((t,r)=>X(()=>{let n,o=Kb(e.map(s=>{let a=Nt(r);return ts(s.channel,a).pipe(I(i=>Hb(g(i),i)),un(i=>i._tag===\"Success\"?n?Le(a,i):(n=i.value[0],Q):Le(a,i)),j(([,i])=>i))}));return $(()=>n??o)}))),WQ=l(2,(e,t)=>cL(e,t)),JQ=l(2,(e,t)=>A(W1(Un(e),t))),KQ=l(2,(e,t)=>A(J1(Un(e),t))),GQ=l(2,(e,t)=>A(K1(Un(e),t))),VQ=l(2,(e,t)=>A(G1(Un(e),t))),x0=l(e=>Ie(e[0]),Se(function*(e,t,r){let n=yield*xi,o=yield*ts(e.channel,n),s=r?.capacity===\"unbounded\"?void 0:r?.capacity??V2,a=yield*Mn({capacity:s}),i=yield*Mn({capacity:s});return yield*dr(function*(){for(;;){let c=yield*o,u=[],f=[];for(let d=0;d0){let d=ng(a,f);d.length>0&&(p=yield*eu(Yi(a,d)))}if(u.length>0){let d=ng(i,u);d.length>0&&(yield*Yi(i,d))}p&&(yield*Ea(p))}}).pipe(Vn(c=>(wa(a,c),wa(i,c),Q)),Ut(n)),[a,i]})),Kp=e=>A(Ce((t,r)=>Tt(Kt(r,Fn(e)),Qi(e)))),ZQ=l(e=>Ie(e[0]),(e,t,r)=>j(x0(Vp(e,n=>t(n),r),n=>n,r),([n,o])=>[Kp(n),Kp(o)])),YQ=l(e=>Ie(e[0]),(e,t,r)=>j(x0(e,t,{capacity:r?.capacity??16}),([n,o])=>[Kp(n),Kp(o)])),QQ=l(2,(e,t)=>t.pipe(j(r=>r?e:Ns),Aa)),XQ=l(2,Se(function*(e,t){let r,n=yield*s0(e.channel),o=rt(n,c=>(r=c,ze(c))),[s,a]=yield*sh(c=>t.transform(o,c)),i=r?Ns:rs(g(n));return a&&(i=nc(Gp(a),i)),[s,i]})),e9=l(2,(e,t)=>A(d2(e.channel,t))),t9=l(2,(e,t)=>A(p2(e.channel,t))),uL=l(2,(e,t)=>e.channel.pipe(tf(r=>t(r).channel),A)),r9=l(2,(e,t)=>e.channel.pipe(Z1(r=>t(r).channel),A)),fL=l(2,(e,t)=>e.channel.pipe(pg(t),A)),Zp=l(2,(e,t)=>A(xo(e.channel,r=>t(r).channel)));var g0=l(2,(e,t)=>e.channel.pipe(Q1(t),A)),n9=l(3,(e,t,r)=>{let n=Array.isArray(t)?o=>k(o,\"_tag\")&&t.includes(o._tag):At(t);return g0(e,o=>n(o)?r(o):Q)}),o9=l(2,(e,t)=>fL(e,r=>{let n=dl(r);return mt(n)?t(n.success):Q})),lL=l(e=>Ie(e[0]),(e,t,r,n)=>A(X1(Un(e),t,o=>r(o).channel,n&&(o=>n(o).channel)))),y0=l(e=>Ie(e[0]),(e,t,r,n)=>A(e2(Un(e),t,o=>r(o).channel,n&&(o=>n(o).channel)))),s9=l(e=>Ie(e[0]),(e,t,r,n)=>{let o=Array.isArray(t)?s=>k(s,\"_tag\")&&t.includes(s._tag):At(t);return lL(e,o,r,n)}),a9=l(e=>Ie(e[0]),(e,t,r)=>{let n;return y0(e,o=>(n??=Object.keys(t),k(o,\"_tag\")&&Hm(o._tag)&&n.includes(o._tag)?B(o):W(o)),o=>t[o._tag](o),r)}),i9=l(e=>Ie(e[0]),(e,t,r,n,o)=>A(t2(Un(e),t,r,(s,a)=>n(s,a).channel,o&&((s,a)=>o(s,a).channel)))),c9=l(e=>Ie(e[0]),(e,t,r,n)=>{let o=Object.create(null);for(let a of Object.keys(r)){let i=r[a];o[a]=(c,u)=>i(c,u).channel}let s=n&&((a,i)=>n(a,i).channel);return A(r2(e.channel,t,o,s))}),u9=l(2,(e,t)=>y0(e,r=>At(r,t)&&k(r,\"reason\")?B(r.reason):W(r),nf)),pL=l(2,(e,t)=>A(n2(e.channel,t))),f9=l(3,(e,t,r)=>A(Y1(e.channel,t,n=>r(n).channel))),l9=l(3,(e,t,r)=>A(dg(e.channel,t,(n,o)=>r(n,o).channel))),p9=l(2,(e,t)=>A(U1(e.channel,r=>Un(t())))),d9=l(2,(e,t)=>Zp(e,r=>rc(t(r)))),m9=e=>A(o2(e.channel)),h9=l(e=>Ie(e[0]),(e,t)=>A(s2(e.channel,t))),x9=l(e=>Ie(e[0]),(e,t)=>A(a2(e.channel,t))),g9=l(2,(e,t)=>A(i2(e.channel,t))),K2=(e,t)=>Aa(j(Kn(t),r=>{let n=Gt.defaultValue(),o=()=>Zp(E0(e,Gt,X(()=>n)),s=>Aa(He(j(r(s),a=>(n=a,Aa(Tt(Rl,o())))),()=>g(nf(s)))));return o()})),y9=l(e=>Ie(e[0]),(e,t,r)=>Wr(()=>{let n=r?.preventFallbackOnPartialStream??!1,o=0,s={attempt:0,stepIndex:0},a=E0(Gm,X(()=>(s={attempt:s.attempt+1,stepIndex:o},s))),i=r?.onEvent===void 0?void 0:$b(r.onEvent,()=>s),c,u=i===void 0?R:d=>kL(vL(d,j(i.begin,m=>{c=m})),m=>$(()=>{if(c===void 0)return Q;let x=c;return c=void 0,i.end(x,m)})),f=v(),p=Wr(()=>{let d=t.steps[o];if(!d)return nf(uy(f));let m=a(u(_L(e,d.provide))),x=!1;if(be(f)){let y=f.value,S=!1,w=m;m=Wr(()=>S?w:(S=!0,nf(y))),m=K2(m,_l(d,!1))}else{let y=_l(d,!0);m=y?K2(m,y):m}return Zp(n?OL(m,y=>(x=!0,Q)):m,y=>(o++,n&&x?nf(y):(f=O(y),p)))});return p})),S9=l(2,(e,t)=>{let r=ot(t);return r===0?Ns:dL(e,(n,o)=>o===r-1)}),b9=l(3,(e,t,r)=>Wr(()=>{let n=ch(t),o=BigInt(0),s=!1;return nc(hL(e,a=>{let i=o+BigInt(a.length);return i>n?(s=!0,!1):(o=i,!0)}),Wr(()=>s?r():Ns))})),E9=l(2,(e,t)=>{let r=ot(t);return r===0?Ns:sc(e,Ps,(n,o)=>($x(n,o),n.length>r&&jx(n,n.length-r),[n,Fs]),{onHalt(n){return Ta(n)}})}),dL=l(e=>Ie(e[0]),(e,t,r)=>Dt(e,(n,o)=>X(()=>{let s=0,a=!1;return I($(()=>a?Y():n),c=>{let u=c.findIndex(f=>t(f,s++));if(u>=0){a=!0;let f=c.slice(0,r?.excludeLast?u:u+1);return tt(f)?g(f):Y()}return g(c)})}))),mL=l(e=>Ie(e[0]),(e,t,r)=>Dt(e,(n,o)=>X(()=>{let s=0,a=!1;return dr(function*(){if(a)return yield*Y();let i=yield*n;for(let c=0;cDt(e,(r,n)=>X(()=>{let o=0,s=!1,a=I($(()=>s?Y():r),i=>{let c=[];for(let u=0;uDt(e,(r,n)=>X(()=>{let o=!1,s=I($(()=>o?Y():r),a=>{let i=[];for(let c=0;cmL(e,(r,n)=>j(t(r,n),o=>!o),{excludeLast:!0})),S0=l(2,(e,t)=>{let r=ot(t);return r===0?e:Dt(e,(n,o)=>X(()=>{let s=0,a=n.pipe(I(i=>s>=r?g(i):(s+=i.length,s<=r?a:g(i.slice(r-s)))));return a}))}),w9=l(2,(e,t)=>S0(xL(e,(r,n)=>!t(r,n)),1)),C9=l(2,(e,t)=>S0(gL(e,(r,n)=>j(t(r,n),o=>!o)),1)),xL=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o=!0,s=0,a=I(r,i=>{let c=i.findIndex(u=>!t(u,s++));return c===-1?a:(o=!1,g(i.slice(c)))});return $(()=>o?a:r)}))),A9=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o=!0,s=I(r,a=>{let i=a.findIndex(c=>ce(t(c)));return i===-1?s:(o=!1,g(a.slice(i)))});return $(()=>o?s:r)}))),gL=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o=!0,s=0,a=dr(function*(){for(;;){let i=yield*r;for(let c=0;co?a:r)}))),k9=l(2,(e,t)=>{let r=ot(t);return r===0?e:Dt(e,(n,o)=>X(()=>{let s=Ps(),a=I(n,i=>{Bx(s,i);let c=s.length-r,u=Vi(s,c);return or(u)?g(u):a});return a}))}),yL=e=>e.channel.pipe(yo(Ee),A),SL=l(2,(e,t)=>(t=Tn(t),Dt(e,(r,n)=>X(()=>{let o=Pt(),s=0,a,i=!1;return $(function c(){if(i)return Y();if(a===void 0)return I(r,u=>{if(o.length===0&&u.length===t)return g(u);if(o.length+u.length{if(o.length===0)return Y();let c=o;return i=!0,o=[],g(c)}))})))),v9=l(2,(e,t)=>bL(e,t,1)),bL=l(3,(e,t,r)=>{let n=Tn(t),o=Tn(r);return Dt(e,(s,a)=>X(()=>{let i=null,c=Ps(),u=!1,f=0,p=zo(s,{onSuccess(d){if(Bx(c,d),f>0){let x=c.length;jx(c,f),f=Math.max(0,f-x)}if(c.length=n;)if(n===o)m.push(Vi(c,n));else{m.push(QD(c,n));let x=c.length;jx(c,o),f=Math.max(0,o-x)}return g(m)},onFailure(d){return c.length===0||u&&c.length<=n-o?ze(d):(i=d,g(Ee(Ta(c))))}});return $(()=>i?ze(i):p)}))}),O9=l(2,(e,t)=>sc(e,Pt,(r,n)=>{let o=Pt();for(let s=0;sQw(gr(e.channel),gr(t.channel),r,n).pipe(yo(Ee),A)),R9=l(4,(e,t,r,n)=>A(Qw(e.channel,t.channel,r,n))),P9=l(e=>Ie(e[0]),(e,t,r,n)=>A(ef(e.channel,t,(o,s)=>{let a=Pt();for(let i=0;iIe(e[0]),(e,t,r,n)=>A(ef(e.channel,t,(o,s)=>{let[a,i]=r(o,s);return o=a,[o,tt(i)?Ee(i):Fs]},n?.onHalt?{onHalt(o){let s=n.onHalt(o);return tt(s)?Ee(s):Fs}}:void 0))),Fs=Pt(),M9=l(e=>Ie(e[0]),(e,t,r,n)=>e.channel.pipe(gr,ef(t,(o,s)=>j(r(o,s),([a,i])=>[a,tt(i)?Ee(i):Pt()]),n?.onHalt?{onHalt(o){let s=n.onHalt(o);return tt(s)?Ee(s):Fs}}:void 0),A)),F9=l(e=>Ie(e[0]),(e,t,r,n)=>e.channel.pipe(ef(t,(o,s)=>j(r(o,s),([a,i])=>[a,tt(i)?Ee(i):Fs]),n?.onHalt?{onHalt(o){let s=n.onHalt(o);return tt(s)?Ee(s):Fs}}:void 0),A)),N9=l(3,(e,t,r)=>Wr(()=>{let n=t();return nc(rc(n),A(ef(e.channel,()=>n,(o,s)=>{let a=Pt();for(let i=0;iWr(()=>e.channel.pipe(gr,V1(t(),r),yo(Ee),A))),D9=l(2,(e,t)=>Dt(e,Se(function*(r,n){let o=yield*Hc,s=Cr(Ge(t)),a,i,c=1/0,u=Pr(),f=Pr(),p=Pr();yield*r.pipe(u.whenOpen,I(m=>(f.openUnsafe(),a=m,c=o.currentTimeMillisUnsafe()+s,Q)),$t({disableYield:!0}),Vn(m=>(i=m,c=o.currentTimeMillisUnsafe(),f.openUnsafe(),p.openUnsafe(),Q)),Ut(n));let d=$(function m(){let x=o.currentTimeMillisUnsafe(),y=c{if(o.currentTimeMillisUnsafe(){if(i){if(a){let m=g(Ee(Bd(a)));return a=void 0,m}return ze(i)}return u.openUnsafe(),f.whenOpen(d)})}))),EL=l(2,(e,t)=>{let r=t.burst??0;return t.strategy===\"enforce\"?L9(e,t.cost,t.units,t.duration,r):$9(e,t.cost,t.units,t.duration,r)}),L9=(e,t,r,n,o)=>Dt(e,s=>Si(a=>{let i=Cr(Ge(n)),c=r+o<0?Number.POSITIVE_INFINITY:r+o,u=r,f=a.currentTimeMillisUnsafe();return g(I(s,function p(d){return I(t(d),m=>{let x=a.currentTimeMillisUnsafe(),S=(x-f)/i,w=u+S*r,E=w<0?c:Math.min(w,c);return m<=E?(u=E-m,f=x,g(d)):I(s,p)})}))})),$9=(e,t,r,n,o)=>Dt(e,s=>Si(a=>{let i=Cr(Ge(n)),c=r+o<0?Number.POSITIVE_INFINITY:r+o,u=r,f=a.monotonicTimeNanosUnsafe();return g(I(s,p=>I(t(p),d=>{let m=a.monotonicTimeNanosUnsafe(),y=Number(m-f)/1e6/i,S=u+y*r,E=Math.min(S,c)-d;if(E>=0)return u=E,f=m,g(p);let U=-E/r,_=Math.max(0,U*i);return _>0?I(ua(_),()=>(u=E,f=m,g(p))):(u=E,f=m,g(p))})))})),B9=l(2,(e,t)=>EL(e,{...t,cost:r=>g(t.cost(r))})),j9=l(2,(e,t)=>yL(SL(e,t))),z9=l(3,(e,t,r)=>b0(e,N2(Tn(t)),Nb(r))),q9=l(e=>Ie(e[0]),(e,t,r)=>IL(e,Se(function*(n,o,s){for(let a=0;aIe(e[0]),(e,t,r)=>Wr(()=>{let n=Up();return IL(e,Se(function*(o,s,a){for(let i=0;ip0(e,Se(function*(n,o,s){let a=yield*o1();yield*Kt(o,Fn(a));let i=Up(),c=yield*L2({lookup:u=>gi(Mn({capacity:r?.bufferSize??4096}).pipe(Rr(f=>(Dp(i,u,f),Sn(a,[u,Kp(f)])))),f=>(Vu(i,u),jw(f))),idleTimeToLive:r?.idleTimeToLive??ls}).pipe(Es(s));return yield*Gn({while:Sr,body:je(I(n,u=>t(u,c,i))),step:$n}).pipe(rt(u=>Bt(a,u)),Ut(o)),Qi(a)})),W9=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o,s,a=Pt(),i=r.pipe(I(u=>{for(let f=0;fc?Y():i),()=>{c=!0;let u=s;return s=void 0,u&&or(u)?g(Ee([o,u])):Y()})}))),J9=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o,s,a=$(()=>{if(s!==void 0){let c=s;return s=void 0,g(c)}return r}).pipe(ca(c=>(o=bs(c),Y()))),i=j($(()=>t.transform(a,n)),([c,u])=>(s=u,Ee(c)));return $(()=>o||i)}))),K9=l(2,(e,t)=>b0(e,t,kl)),b0=l(3,(e,t,r)=>A(go(Se(function*(n,o,s){let a=yield*ts(e.channel,o),i=Pr(!1),c=Symbol(),u=yield*Mn({capacity:0});yield*a.pipe(i.whenOpen,I(h=>(i.closeUnsafe(),Sn(u,h))),$t,rt(h=>Bt(u,h)),Ut(s));let f=yield*Hc,p=yield*di(r),d=Pr(!1),m=f.currentTimeMillisUnsafe(),x=0,y=v(),S,w=h=>(d.openUnsafe()&&(x=f.currentTimeMillisUnsafe()),g(h)),E=d.await.pipe(I(()=>p(m,y)),I(([,h])=>ua(h)),I(()=>Sn(u,c)),He(()=>jw(u)),I(()=>mi)),U=$(()=>{if(S!==void 0){let h=S;return S=void 0,w(h)}return u.state._tag===\"Open\"&&i.openUnsafe(),I(Ms(u),h=>h===c?Y():w(h))}),_=I(([h,b])=>{if(d.isOpen())m+=f.currentTimeMillisUnsafe()-x;else if(u.state._tag===\"Done\")return Y();return y=O(h),S=b,g(Ee(h))});return $(()=>{if(u.state._tag===\"Done\"&&S===void 0)return u.state.exit;d.closeUnsafe();let h=_($(()=>t.transform(U,s)));return u.state._tag===\"Open\"?Xc(h,E):h})})))),G9=l(2,Se(function*(e,t){let r=ot(t.n),n=yield*V9(t),o=new Array(r),s=yield*Or;for(let a=0;aLe(i,u)),A)}return yield*ec(e.channel,a=>Qx(n,a)).pipe(un(a=>Yx(n,a)),tu),o})),V9=e=>gi(e.capacity===\"unbounded\"?Zx(e):e.strategy===\"dropping\"?Gx(e):e.strategy===\"sliding\"?Vx(e):Kx(e),$p),TL=l(2,(e,t)=>j(FL(e,t),eL)),Z9=l(2,(e,t)=>j(H2({acquire:TL(e,t),idleTimeToLive:t.idleTimeToLive}),r=>Aa(W2(r)))),Y9=l(2,(e,t)=>A(Xw(e.channel,t))),Q9=l(2,(e,t)=>A(e0(e.channel,t))),X9=l(2,(e,t)=>e.channel.pipe(e0(F2(t)),N1(([r,n])=>n?ug(n):Xu),A)),e7=e=>Sg(PL(e)),t7=e=>sc(e,Pt,(t,r)=>{let n=gy(t,r);return[n,[n]]}),r7=e=>wL(e,J),wL=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o=!0,s;return I(r,function a(i){let c=[],u=0;for(o&&(o=!1,s=i[0],u=1,c.push(s));uDt(e,(r,n)=>X(()=>{let o=!0,s;return I(r,Se(function*a(i){let c=[],u=0;for(o&&(o=!1,s=i[0],u=1,c.push(s));uIe(e[0]),(e,t)=>Wr(()=>{let r=new TextDecoder(t?.encoding);return Jr(e,n=>r.decode(n,{stream:!0}))})),s7=e=>Wr(()=>{let t=new TextEncoder;return Jr(e,r=>t.encode(r))}),a7=e=>e.channel.pipe(Xw(f2()),A),CL=l(2,(e,t)=>rL(e,(r,n)=>{let o=n===0?[]:[t],s=r.length-1;for(let a=0;arc(t.start).pipe(nc(CL(e,t.middle)),nc(rc(t.end)))),c7=l(2,(e,t)=>AL(e,t,oc(fA([!0,!1])))),AL=l(3,(e,t,r)=>A(Ce(Se(function*(n,o){let s=yield*he(gr(r.channel))(n,o),a=Symbol(),i=!1,c=!1,u=(yield*he(gr(e.channel))(n,o)).pipe(He(()=>(i=!0,g(a)))),f=(yield*he(gr(t.channel))(n,o)).pipe(He(()=>(c=!0,g(a))));return dr(function*(){for(;;){if(i&&c)return yield*Y();let p=yield*s;if(p&&i||!p&&c)continue;let d=yield*p?u:f;if(d!==a)return Ee(d)}})})))),u7=l(2,(e,t)=>A(t0(e.channel,t))),f7=l(2,(e,t)=>A(m2(e.channel,t))),kL=l(2,(e,t)=>A(Hp(e.channel,t))),l7=l(2,(e,t)=>A(h2(e.channel,t))),vL=l(2,(e,t)=>A(x2(e.channel,t))),OL=l(2,(e,t)=>A(r0(e.channel,r=>t(r[0])))),p7=l(2,(e,t)=>A(g2(e.channel,t))),d7=l(2,(e,t)=>A(y2(e.channel,t))),_L=l(e=>Ie(e[0]),(e,t,r)=>A(S2(e.channel,t,r))),m7=l(2,(e,t)=>A(xg(e.channel,t))),h7=l(3,(e,t,r)=>A(gg(e.channel,t,r))),E0=l(3,(e,t,r)=>A(yg(e.channel,t,r))),RL=l(2,(e,t)=>A(b2(e.channel,t))),x7=l(3,(e,t,r)=>RL(e,n=>Jt(n,t,r(Fe(n,t))))),g7=function(){let e=Ie(arguments[0]),t=e?arguments[1]:arguments[0],r=Gs(e?arguments[2]:arguments[1]);if(e){let n=arguments[0];return A(n0(n.channel,t,r))}return n=>A(n0(n.channel,t,r))},y7=rc({}),S7=l(3,(e,t,r)=>Jr(e,n=>({...n,[t]:r(n)})));var b7=l(e=>Ie(e[0]),(e,t,r,n)=>of(e,o=>Jr(r(o),s=>({...o,[t]:s})),n)),E7=l(e=>Ie(e[0]),(e,t,r,n)=>Vp(e,o=>j(r(o),s=>({...o,[t]:s})),n)),I7=l(2,(e,t)=>Jr(e,r=>({[t]:r}))),T7=l(2,(e,t)=>sh(r=>ts(e.channel,r).pipe(I(n=>t.transform(n,r)),j(([n])=>n)))),PL=e=>tc(e.channel,()=>[],(t,r)=>{for(let n=0;ntc(e.channel,()=>0,(t,r)=>t+r.length),C7=e=>tc(e.channel,()=>0,(t,r)=>{for(let n=0;ntc(e.channel,t,(n,o)=>{for(let s=0;sC2(e.channel,t,(n,o)=>{let s=0,a=n;return j(Gn({while:()=>sr(a,o[s]),step(i){a=i,s++}}),()=>a)})),v7=e=>j(T2(e.channel),Rt(yA(0))),O7=e=>j(w2(e.channel),Rt(Bd)),_7=l(2,(e,t)=>ec(e.channel,r=>{let n=0;return Gn({while:()=>nt(r[n++]),step:$n})})),R7=l(2,(e,t)=>I2(e.channel,r=>{let n=!1,o=0;return j(Gn({while:()=>!n&&ot(r[o]),step(s){o++,s||(n=!0)}}),()=>!n)})),ML=l(2,(e,t)=>ec(e.channel,t)),Ig=e=>E2(e.channel),P7=e=>s0(e.channel),M7=e=>tc(e.channel,()=>\"\",(t,r)=>t+r.join(\"\")),F7=e=>j(o0(e.channel),t=>t.buffer),N7=e=>o0(e.channel),I0=l(e=>Ie(e[0]),(e,t,r)=>{let n,o,s=Pr(!1);return new ReadableStream({start(a){o=Nl(On(ML(e,i=>s.whenOpen(X(()=>{s.closeUnsafe();for(let c=0;c{i._tag===\"Failure\"?a.error(Pm(i.cause)):a.close()})},pull(){return new Promise(a=>{n=a,s.openUnsafe()})},cancel(){if(o)return tE(ia(mo(o)))}},r?.strategy)}),U7=l(e=>Ie(e[0]),(e,t)=>I0(e,Ir(),t)),D7=l(e=>Ie(e[0]),(e,t)=>j(hi(),r=>I0(e,r,t))),T0=l(2,(e,t)=>({[Symbol.asyncIterator](){let r=rE(t),n=Rn(t),o=kn(),s,a,i,c,u=p=>{if(c)return c;let d=i;return c=r(Tt(_r(d?mo(d):Q,Le(o,p)),{done:!0,value:void 0})),c},f=async p=>{try{await u(p)}catch(d){await r(nE(\"Suppressed error while closing Stream async iterator\",d))}};return{async next(){if(c)return c;if(a){let m=a.next();if(!m.done)return m;a=void 0}let p=n(s??I(ts(e.channel,o),m=>(s=m,m)));i=p;let d=await r(JD(p));if(i===p&&(i=void 0),Ve(d))return a=d.value[Symbol.iterator](),a.next();if(Bo(d.cause))return u(xt);if(c&&ob(d.cause))return c;throw await f(d),Pm(d.cause)},return(){return u(xt)},async throw(p){throw await f(lb(p)),p}}}})),L7=e=>j(hi(),t=>T0(e,t)),$7=e=>T0(e,Ir()),B7=l(e=>Ie(e[0]),(e,t,r)=>i0(e.channel,t,r)),j7=l(2,(e,t)=>v2(e.channel,t)),FL=l(2,(e,t)=>O2(e.channel,t)),z7=l(2,(e,t)=>A2(e.channel,t)),q7=l(2,(e,t)=>a0(e.channel,t));var w0=lr(\"effect/FileSystem\");var UL=\"~effect/Path\",A0=lr(\"effect/Path\");function DL(e,t){let r=\"\",n=0,o=-1,s=0,a;for(let i=0;i<=e.length;++i){if(i2){let c=r.lastIndexOf(\"/\");if(c!==r.length-1){c===-1?(r=\"\",n=0):(r=r.slice(0,c),n=r.length-1-r.lastIndexOf(\"/\")),o=i,s=0;continue}}else if(r.length===2||r.length===1){r=\"\",n=0,o=i,s=0;continue}}t&&(r.length>0?r+=\"/..\":r=\"..\",n=2)}else r.length>0?r+=\"/\"+e.slice(o+1,i):r=e.slice(o+1,i),n=i-o-1;o=i,s=0}else a===46&&s!==-1?++s:s=-1}return r}function H7(e,t){let r=t.dir||t.root,n=t.base||(t.name||\"\")+(t.ext||\"\");return r?r===t.root?r+n:r+e+n:n}function W7(e){if(e.protocol!==\"file:\")return P(new rf({module:\"Path\",method:\"fromFileUrl\",description:\"URL must be of scheme file\"}));if(e.hostname!==\"\")return P(new rf({module:\"Path\",method:\"fromFileUrl\",description:\"Invalid file URL host\"}));let t=e.pathname;for(let r=0;r=-1&&!r;o--){let s;if(o>=0)s=arguments[o];else{let a=globalThis.process;n===void 0&&\"process\"in globalThis&&typeof a==\"object\"&&a!==null&&typeof a.cwd==\"function\"&&(n=a.cwd()),s=n}s.length!==0&&(t=s+\"/\"+t,r=s.charCodeAt(0)===47)}return t=DL(t,!r),r?t.length>0?\"/\"+t:\"/\":t.length>0?t:\".\"},J7=47;function K7(e){let t=new URL(\"file://\"),r=LL(e);return e.charCodeAt(e.length-1)===J7&&r[r.length-1]!==\"/\"&&(r+=\"/\"),t.pathname=X7(r),g(t)}var G7=/%/g,V7=/\\\\/g,Z7=/\\n/g,Y7=/\\r/g,Q7=/\\t/g;function X7(e){return e.includes(\"%\")&&(e=e.replace(G7,\"%25\")),e.includes(\"\\\\\")&&(e=e.replace(V7,\"%5C\")),e.includes(`\n`)&&(e=e.replace(Z7,\"%0A\")),e.includes(\"\\r\")&&(e=e.replace(Y7,\"%0D\")),e.includes(\"\t\")&&(e=e.replace(Q7,\"%09\")),e}var C0=A0.of({[UL]:UL,resolve:LL,normalize(e){if(e.length===0)return\".\";let t=e.charCodeAt(0)===47,r=e.charCodeAt(e.length-1)===47;return e=DL(e,!t),e.length===0&&!t&&(e=\".\"),e.length>0&&r&&(e+=\"/\"),t?\"/\"+e:e},isAbsolute(e){return e.length>0&&e.charCodeAt(0)===47},join(){if(arguments.length===0)return\".\";let e;for(let t=0;t0&&(e===void 0?e=r:e+=\"/\"+r)}return e===void 0?\".\":C0.normalize(e)},relative(e,t){if(e===t||(e=C0.resolve(e),t=C0.resolve(t),e===t))return\"\";let r=1;for(;rc){if(t.charCodeAt(s+f)===47)return t.slice(s+f+1);if(f===0)return t.slice(s+f)}else o>c&&(e.charCodeAt(r+f)===47?u=f:f===0&&(u=0));break}let d=e.charCodeAt(r+f),m=t.charCodeAt(s+f);if(d!==m)break;d===47&&(u=f)}let p=\"\";for(f=r+u+1;f<=n;++f)(f===n||e.charCodeAt(f)===47)&&(p.length===0?p+=\"..\":p+=\"/..\");return p.length>0?p+t.slice(s+u):(s+=u,t.charCodeAt(s)===47&&++s,t.slice(s))},dirname(e){if(e.length===0)return\".\";let t=e.charCodeAt(0),r=t===47,n=-1,o=!0;for(let s=e.length-1;s>=1;--s)if(t=e.charCodeAt(s),t===47){if(!o){n=s;break}}else o=!1;return n===-1?r?\"/\":\".\":r&&n===1?\"//\":e.slice(0,n)},basename(e,t){let r=0,n=-1,o=!0,s;if(t!==void 0&&t.length>0&&t.length<=e.length){if(t.length===e.length&&t===e)return\"\";let a=t.length-1,i=-1;for(s=e.length-1;s>=0;--s){let c=e.charCodeAt(s);if(c===47){if(!o){r=s+1;break}}else i===-1&&(o=!1,i=s+1),a>=0&&(c===t.charCodeAt(a)?--a===-1&&(n=s):(a=-1,n=i))}return r===n?n=i:n===-1&&(n=e.length),e.slice(r,n)}else{for(s=e.length-1;s>=0;--s)if(e.charCodeAt(s)===47){if(!o){r=s+1;break}}else n===-1&&(o=!1,n=s+1);return n===-1?\"\":e.slice(r,n)}},extname(e){let t=-1,r=0,n=-1,o=!0,s=0;for(let a=e.length-1;a>=0;--a){let i=e.charCodeAt(a);if(i===47){if(!o){r=a+1;break}continue}n===-1&&(o=!1,n=a+1),i===46?t===-1?t=a:s!==1&&(s=1):t!==-1&&(s=-1)}return t===-1||n===-1||s===0||s===1&&t===n-1&&t===r+1?\"\":e.slice(t,n)},format:function(t){if(t===null||typeof t!=\"object\")throw new TypeError('The \"pathObject\" argument must be of type Object. Received type '+typeof t);return H7(\"/\",t)},parse(e){let t={root:\"\",dir:\"\",base:\"\",ext:\"\",name:\"\"};if(e.length===0)return t;let r=e.charCodeAt(0),n=r===47,o;n?(t.root=\"/\",o=1):o=0;let s=-1,a=0,i=-1,c=!0,u=e.length-1,f=0;for(;u>=o;--u){if(r=e.charCodeAt(u),r===47){if(!c){a=u+1;break}continue}i===-1&&(c=!1,i=u+1),r===46?s===-1?s=u:f!==1&&(f=1):s!==-1&&(f=-1)}return s===-1||i===-1||f===0||f===1&&s===i-1&&s===a+1?i!==-1&&(a===0&&n?t.base=t.name=e.slice(1,i):t.base=t.name=e.slice(a,i)):(a===0&&n?(t.name=e.slice(1,s),t.base=e.slice(1,i)):(t.name=e.slice(a,s),t.base=e.slice(a,i)),t.ext=e.slice(s,i)),a>0?t.dir=e.slice(0,a-1):n&&(t.dir=\"/\"),t},sep:\"/\",fromFileUrl:W7,toFileUrl:K7,toNamespacedPath:R});function Yp(e){return{_tag:\"Value\",value:e}}function wg(e,t){return{_tag:\"Record\",keys:e,value:t}}function O0(e,t){return{_tag:\"Array\",length:e,value:t}}var Tg=class extends Fr(\"SourceError\"){},ac=we(\"effect/ConfigProvider\",{defaultValue:()=>WL()}),eX={...rr,toJSON(){return{_id:\"ConfigProvider\"}}},tX=e=>e;function BL(e,t){let r=Object.create(eX);return r.load=e,r.mapInput=t,r}function jL(e,t){return BL(r=>e(t(r)),r=>jL(e,n=>r(t(n))))}function zL(e,t){return BL(r=>I(e.load(r),n=>n!==void 0?g(n):t.load(r)),r=>zL(e.mapInput(r),t.mapInput(r)))}function Cg(e){return jL(e,tX)}var v0=l(2,(e,t)=>zL(e,t)),_0=l(2,(e,t)=>e.mapInput(t)),rX=_0(e=>e.map(t=>typeof t==\"number\"?t:A_(t))),nX=l(2,(e,t)=>{let r=typeof t==\"string\"?[t]:t;return _0(e,n=>[...r,...n])}),oX=e=>jo(e)?yb(ac)(e):n_(ac)(e),sX=(e,t)=>yb(ac)(dr(function*(){let r=yield*ac,n=jo(e)?yield*e:e;return t?.asPrimary?v0(n,r):v0(r,n)}));function aX(e,t){let r=t?.preserveEmptyStrings===!0;return Cg(n=>g(iX(e,n,r)))}function iX(e,t,r){let n=e;for(let o of t){if(n==null)return;if(Array.isArray(n)){if(typeof o!=\"number\"||!Number.isInteger(o)||o<0||o>=n.length)return;n=n[o];continue}if(fr(n)){if(typeof o!=\"string\"||!Object.hasOwn(n,o))return;n=n[o];continue}return}return cX(n,r)}function cX(e,t){if(e!=null)return typeof e==\"string\"?qL(e,t):typeof e==\"number\"||typeof e==\"boolean\"||typeof e==\"bigint\"?Yp(String(e)):Array.isArray(e)?O0(e.length):fr(e)?wg(new Set(Object.keys(e))):Yp(F(e))}function qL(e,t){let r=HL(e,t);return r===void 0?void 0:Yp(r)}function HL(e,t){return e===\"\"&&!t?void 0:e}function R0(e,t){let r=t?.preserveEmptyStrings===!0,n=uX(e);return Cg(o=>g(fX(n,e,o,r)))}function WL(e){let t=e?.env??{...globalThis.process?.env,...import.meta?.env};return R0(t,{preserveEmptyStrings:e?.preserveEmptyStrings})}function uX(e){let t={};for(let[r,n]of Object.entries(e)){if(n===void 0)continue;let o=r.split(\"_\"),s=t;for(let a of o){let i=s.children??=Object.create(null);s=i[a]??={}}}return t}function fX(e,t,r,n){let o=r.map(String).join(\"_\"),s=HL(Object.hasOwn(t,o)?t[o]:void 0,n),a=lX(e,r),i=a?.children?Object.keys(a.children):[];if(i.length===0)return s===void 0?void 0:Yp(s);if(i.every(Cc)){let u=Math.max(...i.map(f=>parseInt(f,10)))+1;return O0(u,s)}return wg(new Set(i),s)}function lX(e,t){if(t.length===0)return e;let r=e;for(let n of t)if(r=r?.children?.[String(n)],!r)return;return r}function JL(e,t){let r=dX(e);return t?.expandVariables&&(r=mX(r)),R0(r,{preserveEmptyStrings:t?.preserveEmptyStrings})}var pX=/(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?\\s*(?:#.*)?(?:$|$)/mg;function dX(e){let t=Object.create(null);e=e.replace(/\\r\\n?/gm,`\n`);let r;for(;(r=pX.exec(e))!=null;){let n=r[1],o=r[2]||\"\";o=o.trim();let s=o[0];o=o.replace(/^(['\"`])([\\s\\S]*)\\1$/gm,\"$2\"),s==='\"'&&(o=o.replace(/\\\\n/g,`\n`),o=o.replace(/\\\\r/g,\"\\r\")),t[n]=o}return t}function mX(e){let t=Object.create(null);for(let r of Object.keys(e))t[r]=KL(e[r],e).replace(/\\\\\\$/g,\"$\");return t}function KL(e,t){let r=hX(e,/(?!(?<=\\\\))\\$/g);if(r===-1)return e;let n=e.slice(r),o=/((?!(?<=\\\\))\\${?([\\w]+)(?::-([^}\\\\]*))?}?)/,s=n.match(o);if(s!==null){let[a,i,c,u]=s,f=Object.hasOwn(t,c)&&t[c]!==\"\"?t[c]:u??\"\";return KL(e.replace(i,()=>f),t)}return e}function hX(e,t){let r=Array.from(e.matchAll(t));return r.length>0?r.slice(-1)[0].index:-1}var xX=Se(function*(e){let r=yield*(yield*w0).readFileString(e?.path??\".env\");return JL(r,e)}),gX=Se(function*(e){let t=yield*A0,r=yield*w0,n=e?.rootPath??\"/\",o=e?.preserveEmptyStrings===!0;return Cg(s=>{let a=t.join(n,...s.map(String)),i=r.readFileString(a).pipe(j(u=>qL(u.trim(),o))),c=r.readDirectory(a).pipe(j(u=>wg(new Set(u.map(f=>t.basename(f))))));return i.pipe(ca(u=>c.pipe(ca(f=>k0(u)&&k0(f)?g(void 0):P(k0(u)?f:u)))),th(u=>new Tg({message:`Failed to read file at ${t.join(n,...s.map(String))}`,cause:u})))})}),k0=e=>e.reason._tag===\"NotFound\";var GL=[\"All\",\"Fatal\",\"Error\",\"Warn\",\"Info\",\"Debug\",\"Trace\",\"None\"];var M0=\"~effect/Config\",YL=e=>k(e,M0),af=class{_tag=\"ConfigError\";name=\"ConfigError\";cause;constructor(t){this.cause=t}get message(){return this.cause.toString()}toString(){return`ConfigError(${this.message})`}},yX={...yd({label:\"Config\",evaluate(e){return this.parse(e.getRef(ac))}}),[M0]:M0,toJSON(){return{_id:\"Config\"}}};function os(e){let t=Object.create(yX);return t.evaluator=e,t.parse=r=>st(e(r,[]),Pl),t}var So=(e,t,r)=>e.evaluator(t,r),QL=l(2,(e,t)=>os((r,n)=>j(So(e,r,n),Tr(t)))),SX=l(2,(e,t)=>os((r,n)=>I(So(e,r,n),wr({onSuccess:o=>So(t(o),r,n),onFailure:o=>g(W(o))})))),bX=l(2,(e,t)=>os((r,n)=>I(So(e,r,n),o=>mt(o)?Dr(t(o.success),B):g(W(o.failure))))),EX=l(2,(e,t)=>os((r,n)=>zo(So(e,r,n),{onFailure:o=>{let s=ib(o);return be(s)&&o.reasons.every(na)?So(t(s.value),r,n):ze(o)},onSuccess:o=>ce(o)?So(t(o.failure),r,n):g(o)})));function XL(e){let t=globalThis.Array.isArray(e)?e:Symbol.iterator in e?[...e]:e;return os((r,n)=>globalThis.Array.isArray(t)?Dr(Ym(t.map(o=>So(o,r,n))),hy):Dr(Ym(Ya(t,o=>So(o,r,n))),hy))}var e$=l(2,(e,t)=>os((r,n)=>Dr(So(e,r,n),o=>ce(o)?B(t):o))),IX=e=>e.pipe(QL(O),e$(v())),t$=e=>YL(e)?e:XL(Ya(e,t=>t$(t))),TX=e=>At(e,\"SourceError\"),wX=()=>\"\",r$=e=>t=>rt(t,r=>ze(oa(r.reasons.map(e)))),n$=(e,t)=>e.load(t).pipe(r$(r=>na(r)?zc(r.error).annotate(Mm(r)):r),Dr(r=>({provider:e,path:t,node:r,toString:wX}))),ZL=(e,t)=>n$(e.provider,[...e.path,t]),P0=(e,t)=>ga(eo,e,new ve(gt(r=>t(r)),jr())),o$=e=>{switch(e._tag){case\"Union\":return e.types.every(o$);case\"Objects\":case\"Arrays\":case\"Suspend\":return!1;default:return!0}},F0=(e,t)=>{switch(e._tag){case\"Objects\":return t?._tag===\"Record\";case\"Arrays\":return t?._tag===\"Array\";case\"Union\":return e.types.some(r=>F0(r,t));case\"Suspend\":return F0(e.thunk(),t);default:return t?.value!==void 0}},CX=Vr(e=>{let t=new WeakSet,r=oF(n=>{switch(t.add(n),n._tag){case\"Objects\":{let o=n.indexSignatures.map(a=>Ui(a.parameter)),s=Se(function*(a){if(a.node?._tag!==\"Record\")return;let i=a.node,c=new Set;for(let f of n.propertySignatures)typeof f.name==\"string\"&&c.add(f.name);if(o.length>0)for(let f of i.keys)o.some(p=>p(f))&&c.add(f);let u={};for(let f of c){let p=yield*ZL(a,f);p.node!==void 0&&M(u,f,p)}return u});return P0(n.recur(r,a=>a),s)}case\"Arrays\":{let o=Se(function*(s){if(s.node?._tag!==\"Array\")return;let a=[];for(let i=0;ig(o.node?.value))});return r(e)});function tr(e,t){let r=Np(e),n=hn(r.ast),o=nt(q(CX(r.ast))),s=typeof t==\"string\"?[t]:t??[];return os((a,i)=>{let c=[...i,...s];return n$(a,c).pipe(st(u=>o(u).pipe(Dr(B),ru(f=>{let p=new af(new Os(c.length>0?new ut(c,f):f));return F0(n,u.node)?P(p):g(W(p))}))),r$(u=>rb(u)&&TX(u.defect)?jc(new af(u.defect)).annotate(Mm(u)):u))})}var AX=lo.check(Hu({minimum:1,maximum:65535})),kX=fo(GL),s$=e=>br(e)||globalThis.Array.isArray(e);function vX(e){return os(()=>P(new af(e)))}function OX(e){return os(()=>g(B(e)))}function _X(e){return tr(K,e)}function RX(e){return tr(fw,e)}function PX(e){return tr(Wi,e)}function MX(e){return tr(qu,e)}function FX(e){return tr(lo,e)}function NX(e,t){return tr(Ct(e),t)}function UX(e,t){return tr(fo(e),t)}function DX(e,t,r){let n=s$(t),o=n?r:t,s=$e(e),a=o?.separator??\",\",i=K.pipe(G(Np(s),{decode:zP(o),encode:Ch(jr({strict:!1}),oe(c=>c.join(a)))}));return tr(it([i,s]),n?t:void 0)}function LX(e,t,r,n){let o=s$(r),s=_s(e,t),a=oM(o?n:r),i=K.pipe(G(Np(s),{decode:a.decode,encode:Ch(jr({strict:!1}),a.encode)}));return tr(it([s,i]),o?r:void 0)}function $X(e){return tr(nw,e)}function BX(e){return tr(hw,e)}function jX(e){return tr(Ju,e)}function zX(e){return tr(AX,e)}function qX(e){return tr(kX,e)}function HX(e){return tr(Fx(K),e)}function WX(e){return tr(Ox,e)}function JX(e){return tr(Wu,e)}var KX=l(2,(e,t)=>os((r,n)=>So(e,r,[...n,t])));var ic={};Eo(ic,{Console:()=>a$,assert:()=>GX,clear:()=>VX,consoleWith:()=>jt,count:()=>ZX,countReset:()=>YX,debug:()=>QX,dir:()=>XX,dirxml:()=>eee,error:()=>tee,group:()=>ree,info:()=>nee,log:()=>oee,table:()=>see,time:()=>aee,timeLog:()=>iee,trace:()=>cee,warn:()=>uee,withGroup:()=>fee,withTime:()=>lee});var a$=VS,jt=e=>re(t=>e(t.getRef(a$))),GX=(e,...t)=>jt(r=>z(()=>{r.assert(e,...t)})),VX=jt(e=>z(()=>{e.clear()})),ZX=e=>jt(t=>z(()=>{t.count(e)})),YX=e=>jt(t=>z(()=>{t.countReset(e)})),QX=(...e)=>jt(t=>z(()=>{t.debug(...e)})),XX=(e,t)=>jt(r=>z(()=>{r.dir(e,t)})),eee=(...e)=>jt(t=>z(()=>{t.dirxml(...e)})),tee=(...e)=>jt(t=>z(()=>{t.error(...e)})),ree=e=>jt(t=>ai(z(()=>{e?.collapsed?t.groupCollapsed(e.label):t.group(e?.label)}),()=>z(()=>{t.groupEnd()}))),nee=(...e)=>jt(t=>z(()=>{t.info(...e)})),oee=(...e)=>jt(t=>z(()=>{t.log(...e)})),see=(e,t)=>jt(r=>z(()=>{r.table(e,t)})),aee=e=>jt(t=>ai(z(()=>{t.time(e)}),()=>z(()=>{t.timeEnd(e)}))),iee=(e,...t)=>jt(r=>z(()=>{r.timeLog(e,...t)})),cee=(...e)=>jt(t=>z(()=>{t.trace(...e)})),uee=(...e)=>jt(t=>z(()=>{t.warn(...e)})),fee=l(e=>ie(e[0]),(e,t)=>jt(r=>ul(z(()=>{t?.collapsed?r.groupCollapsed(t.label):r.group(t?.label)}),()=>e,()=>z(()=>{r.groupEnd()})))),lee=l(e=>ie(e[0]),(e,t)=>jt(r=>ul(z(()=>{r.time(t)}),()=>e,()=>z(()=>{r.timeEnd(t)}))));var Kr={OK:200,Unauthorized:401,NotFound:404,InternalServerError:500,ServiceUnavailable:503},N0={[Kr.OK]:\"OK\",[Kr.Unauthorized]:\"Unauthorized\",[Kr.NotFound]:\"Not Found\",[Kr.InternalServerError]:\"Internal Server Error\",[Kr.ServiceUnavailable]:\"Service Unavailable\"},pee=e=>{let t=e.startsWith(\"/\"),r=[];for(let o of e.split(\"/\"))if(!(o===\"\"||o===\".\")){if(o===\"..\"){r.length>0&&r.at(-1)!==\"..\"?r.pop():t||r.push(\"..\");continue}r.push(o)}let n=r.join(\"/\");return t?`/${n}`:n||\".\"},ka=(...e)=>pee(e.filter(Boolean).join(\"/\")),cf=e=>{if(e.length===0)return\".\";let t=e.length;for(;t>1&&e[t-1]===\"/\";)t-=1;let r=e.slice(0,t),n=r.lastIndexOf(\"/\");return n<0?\".\":n===0?\"/\":r.slice(0,n)},i$=e=>{if(!e.startsWith(\"/\"))throw new TypeError(\"Path must be absolute\");let t=new URL(\"file:///\");return t.pathname=e.replace(/%/g,\"%25\").replace(/\\\\/g,\"%5C\"),t};var dee=/^[A-Za-z0-9_-]+$/u,c$=/[*?[{]/u,Ag=(e,t)=>t===e||t.startsWith(`${e.replace(/\\/+$/u,\"\")}/`),uf=class extends Cn.TaggedError(\"FunctionFileSystemError\"){},Dn=(e,t)=>e.lstat(t).pipe(N.catch(()=>N.undefined)),cc=(e,t,r)=>N.all([e.realPath(t),e.realPath(r)],{concurrency:2}).pipe(N.map(([n,o])=>Ag(n,o)),N.orElseSucceed(()=>!1)),u$=(e,t,r)=>N.gen(function*(){let n=yield*Dn(e,r);if(n===void 0)return!0;if(n.isSymbolicLink||!(yield*cc(e,t,r)))return!1;if(!n.isDirectory)return!0;let o=yield*e.readDirectory(r).pipe(N.catch(()=>N.undefined));if(o===void 0)return!1;for(let s of o)if(!(yield*u$(e,t,ka(r,s))))return!1;return!0}),Qp=(e,t)=>t.length===0?\"\":t.startsWith(\"/\")?t:ka(e,t),U0=[\"deno.json\",\"deno.jsonc\"],f$=e=>U0.includes(e.slice(e.lastIndexOf(\"/\")+1)),mee=(e,t,r)=>N.gen(function*(){let n=cf(ka(r));for(;Ag(t,n);){for(let s of U0){let a=ka(n,s),i=yield*Dn(e,a);if(i!==void 0)return i.isFile&&!i.isSymbolicLink?a:void 0}let o=cf(n);if(o===n)break;n=o}}),hee=(e,t,r)=>N.all([e.realPath(t),e.realPath(r)],{concurrency:2}).pipe(N.map(([n,o])=>n===o),N.orElseSucceed(()=>!1)),l$=N.fn(\"Functions.resolveFunctionConfig\")(function*(e){let{root:t,slug:r,overrides:n,fs:o}=e;if(!t.startsWith(\"/\")||!dee.test(r)||r===\"_shared\"||(yield*Dn(o,t))===void 0)return;let a=yield*o.realPath(t).pipe(N.orElseSucceed(()=>\"\"));if(!a.startsWith(\"/\"))return;let i=yield*Dn(o,a);if(i===void 0||!i.isDirectory)return;let c=e.filesRoot===void 0?a:yield*o.realPath(e.filesRoot).pipe(N.orElseSucceed(()=>\"\"));if(!c.startsWith(\"/\")||!Ag(c,a))return;let u=n.$default,f=n[r],p={...u,...f};if(p.enabled===!1)return;let d=ka(a,r),m=p?.entrypointPath&&p.entrypointPath.length>0?p.entrypointPath:p.entrypoint&&p.entrypoint.length>0?p.entrypoint:\"index.ts\";if(!m.startsWith(\"/\")){let b=yield*Dn(o,d);if(b===void 0||!b.isDirectory||!(yield*cc(o,a,d)))return}let x=Qp(d,m);if(!(yield*cc(o,c,x)))return;let y=yield*Dn(o,x);if(y===void 0||!y.isFile||y.isSymbolicLink)return;let S=f?.importMapPath??f?.import_map,w=u?.importMapRoot??u?.import_map_root,E=S!==void 0?Qp(d,S):w!==void 0?Qp(a,w):Qp(d,\"\");if(E.length>0){if(!(yield*cc(o,c,E)))return;let b=yield*Dn(o,E);if(b===void 0||!b.isFile||b.isSymbolicLink)return}else for(let b of U0){let T=ka(d,b),C=yield*Dn(o,T);if(C!==void 0){if(!C.isFile||C.isSymbolicLink||!(yield*cc(o,c,T)))return;E=T;break}}let U=E.length===0?void 0:yield*mee(o,c,x),_=U!==void 0&&(yield*hee(o,U,E)),h=(p.staticFiles??p.static_files??[]).map(b=>Qp(d,b));for(let b of h){if(!Ag(c,b))return;let T=b.search(c$),C=T<0?b:b.slice(0,T),L=T<0?cf(b):C.slice(0,Math.max(0,C.lastIndexOf(\"/\")))||a;if(!(yield*u$(o,c,L)))return;if(!c$.test(b)){let ee=yield*Dn(o,b);if(ee!==void 0&&(!(yield*cc(o,c,b))||ee.isSymbolicLink))return}}return{entrypointPath:x,importMapPath:E,importMapDiscoveredByRuntime:_,staticFiles:h,verifyJWT:p.verifyJWT??p.verify_jwt??!0,env:p.env}}),xee=e=>ka(cf(e.entrypointPath),\"package.json\"),p$=e=>{let t=new Map,r=new Map;return(n,o)=>{let s=r.get(n);if(s!==void 0)return s;let a=cf(o.entrypointPath),i=t.get(a),c=i===void 0||i===n?a:e();return i===void 0&&t.set(a,n),r.set(n,c),c}},d$=N.fn(\"Functions.packageJsonContainedFor\")(function*(e){if(!e.root.startsWith(\"/\"))return!1;let t=yield*Dn(e.fs,e.root);if(t===void 0||!t.isDirectory&&!t.isSymbolicLink)return!1;let r=yield*e.fs.realPath(e.root).pipe(N.orElseSucceed(()=>\"\"));if(!r.startsWith(\"/\"))return!1;let n=yield*Dn(e.fs,r);if(n===void 0||!n.isDirectory)return!1;let o=xee(e.config),s=yield*Dn(e.fs,o);return s===void 0||!s.isFile||s.isSymbolicLink?!1:yield*cc(e.fs,r,o)});var D0=new TextEncoder,Xp=new TextDecoder,kg=new TextDecoder(\"utf-8\",{fatal:!0}),efe=2**32;function m$(...e){let t=e.reduce((o,{length:s})=>o+s,0),r=new Uint8Array(t),n=0;for(let o of e)r.set(o,n),n+=o.length;return r}function ff(e){let t=new Uint8Array(e.length);for(let r=0;r127)throw new TypeError(\"non-ASCII string encountered in encode()\");t[r]=n}return t}var vg=(e,t=\"algorithm.name\")=>new TypeError(`CryptoKey does not support this operation, its ${t} must be ${e}`);function gee(e,t){if(t&&!e.usages.includes(t))throw new TypeError(`CryptoKey does not support this operation, its usages must include ${t}.`)}function h$(e,t){let{modulusLength:r}=t.algorithm;if(typeof r!=\"number\"||r<2048)throw new TypeError(`${e} requires key modulusLength to be 2048 bits or larger`)}function x$(e,t,r){let n=e.algorithm;if(n.name!==t.name)throw vg(t.name);if(t.hash&&n.hash?.name!==t.hash)throw vg(t.hash,\"algorithm.hash\");if(t.namedCurve&&n.namedCurve!==t.namedCurve)throw vg(t.namedCurve,\"algorithm.namedCurve\");if(t.length!==void 0&&n.length!==t.length)throw vg(t.length,\"algorithm.length\");gee(e,r)}function yee(e,t,...r){if(r.length>2){let n=r.pop();e+=`one of type ${r.join(\", \")}, or ${n}.`}else r.length===2?e+=`one of type ${r[0]} or ${r[1]}.`:e+=`of type ${r[0]}.`;return t==null?e+=` Received ${t}`:typeof t==\"function\"&&t.name?e+=` Received function ${t.name}`:typeof t==\"object\"&&t!=null&&t.constructor?.name&&(e+=` Received an instance of ${t.constructor.name}`),e}var ed=(e,t,...r)=>yee(`Key for the ${e} algorithm must be `,t,...r);var yr=class extends Error{static code=\"ERR_JOSE_GENERIC\";code=\"ERR_JOSE_GENERIC\";constructor(t,r){super(t,r),this.name=this.constructor.name,Error.captureStackTrace?.(this,this.constructor)}},Us=class extends yr{static code=\"ERR_JWT_CLAIM_VALIDATION_FAILED\";code=\"ERR_JWT_CLAIM_VALIDATION_FAILED\";claim;reason;payload;constructor(t,r,n=\"unspecified\",o=\"unspecified\"){super(t,{cause:{claim:n,reason:o,payload:r}}),this.claim=n,this.reason=o,this.payload=r}},td=class extends yr{static code=\"ERR_JWT_EXPIRED\";code=\"ERR_JWT_EXPIRED\";claim;reason;payload;constructor(t,r,n=\"unspecified\",o=\"unspecified\"){super(t,{cause:{claim:n,reason:o,payload:r}}),this.claim=n,this.reason=o,this.payload=r}},Og=class extends yr{static code=\"ERR_JOSE_ALG_NOT_ALLOWED\";code=\"ERR_JOSE_ALG_NOT_ALLOWED\"},bo=class extends yr{static code=\"ERR_JOSE_NOT_SUPPORTED\";code=\"ERR_JOSE_NOT_SUPPORTED\"};var Gr=class extends yr{static code=\"ERR_JWS_INVALID\";code=\"ERR_JWS_INVALID\"},lf=class extends yr{static code=\"ERR_JWT_INVALID\";code=\"ERR_JWT_INVALID\"};var rd=class extends yr{static code=\"ERR_JWKS_INVALID\";code=\"ERR_JWKS_INVALID\"},pf=class extends yr{static code=\"ERR_JWKS_NO_MATCHING_KEY\";code=\"ERR_JWKS_NO_MATCHING_KEY\";constructor(t=\"no applicable key found in the JSON Web Key Set\",r){super(t,r)}},_g=class extends yr{[Symbol.asyncIterator]=async function*(){};static code=\"ERR_JWKS_MULTIPLE_MATCHING_KEYS\";code=\"ERR_JWKS_MULTIPLE_MATCHING_KEYS\";constructor(t=\"multiple matching keys found in the JSON Web Key Set\",r){super(t,r)}},Rg=class extends yr{static code=\"ERR_JWKS_TIMEOUT\";code=\"ERR_JWKS_TIMEOUT\";constructor(t=\"request timed out\",r){super(t,r)}},Pg=class extends yr{static code=\"ERR_JWS_SIGNATURE_VERIFICATION_FAILED\";code=\"ERR_JWS_SIGNATURE_VERIFICATION_FAILED\";constructor(t=\"signature verification failed\",r){super(t,r)}};var L0=e=>{if(e?.[Symbol.toStringTag]===\"CryptoKey\")return!0;try{return e instanceof CryptoKey}catch{return!1}},See=e=>e?.[Symbol.toStringTag]===\"KeyObject\",g$=e=>L0(e)||See(e);function y$(e){if(Uint8Array.fromBase64)return Uint8Array.fromBase64(e);let t=atob(e),r=new Uint8Array(t.length);for(let n=0;ntypeof o!=\"string\")||new Set(n).size!==n.length)throw new TypeError('\"key_ops\" (Key Operations) Parameter must be an array of unique strings');t.key_ops=n}return t}var z0=e=>e[Symbol.toStringTag],bee=(e,t,r)=>{let{alg:n}=e;if(t.use!==void 0){let o=r===\"sign\"||r===\"verify\"?\"sig\":\"enc\";if(t.use!==o)throw new TypeError(`Invalid key for this operation, its \"use\" must be \"${o}\" when present`)}if(t.alg!==void 0&&t.alg!==n)throw new TypeError(`Invalid key for this operation, its \"alg\" must be \"${n}\" when present`);if(Array.isArray(t.key_ops)){let o=r===\"encrypt\"||r===\"decrypt\"?e.ops?.[r===\"encrypt\"?0:1]:r;if(o&&!t.key_ops.includes(o))throw new TypeError(`Invalid key for this operation, its \"key_ops\" must include \"${o}\" when present`)}};function Eee(e,t,r){let{alg:n,secret:o}=e,s=r===\"decrypt\"||r===\"sign\";if(o&&t instanceof Uint8Array)return[I$,t];if(va(t)){let a=b$(t);if(typeof a.kty!=\"string\")throw new TypeError(o?ed(n,t,\"CryptoKey\",\"KeyObject\",\"JSON Web Key\",\"Uint8Array\"):ed(n,t,\"CryptoKey\",\"KeyObject\",\"JSON Web Key\"));if(!(o?a.kty===\"oct\"&&typeof a.k==\"string\":a.kty!==\"oct\"&&(s?a.kty===\"AKP\"&&typeof a.priv==\"string\"||typeof a.d==\"string\":a.d===void 0&&a.priv===void 0)))throw new TypeError(o?'JSON Web Key for symmetric algorithms must have JWK \"kty\" (Key Type) equal to \"oct\" and the JWK \"k\" (Key Value) present':`JSON Web Key for this operation must be a ${s?\"private\":\"public\"} JWK`);return bee(e,a,r),[C$,t,a]}if(!g$(t))throw new TypeError(o?ed(n,t,\"CryptoKey\",\"KeyObject\",\"JSON Web Key\",\"Uint8Array\"):ed(n,t,\"CryptoKey\",\"KeyObject\",\"JSON Web Key\"));if(o){if(t.type!==\"secret\")throw new TypeError(`${z0(t)} instances for symmetric algorithms must be of type \"secret\"`)}else{if(t.type===\"secret\")throw new TypeError(`${z0(t)} instances for asymmetric algorithms must not be of type \"secret\"`);let a=s?\"private\":\"public\";if((t.type===\"public\"||t.type===\"private\")&&t.type!==a){let i=r===\"sign\"?\"signing\":r===\"verify\"?\"verifying\":`${r.slice(0,-1)}tion`;throw new TypeError(`${z0(t)} instances for asymmetric algorithm ${i} must be of type \"${a}\"`)}}return L0(t)?[T$,t]:[w$,t]}var I$=0,T$=1,w$=2,C$=3,q0,Iee={__proto__:null,prime256v1:\"P-256\",secp384r1:\"P-384\",secp521r1:\"P-521\"};function Ug(e,t,r){q0||=new WeakMap;let n=q0.get(e);return r&&(n?n[t]=r:q0.set(e,{[t]:r})),r??n?.[t]}var E$=async(e,t,r)=>Ug(e,r.alg)??Ug(e,r.alg,await Ng(r,{...t,alg:r.alg})),Tee=(e,t)=>{let r=Ug(e,t.alg);if(r)return r;let n=e.type===\"public\",o=t.usages[n?0:1],{asymmetricKeyType:s}=e,a=Iee[e.asymmetricKeyDetails?.namedCurve],i=t.resolve?.({crv:a,asymmetricKeyType:s})??t.subtle;return Ug(e,t.alg,e.toCryptoKey(i,n,o))};async function A$(e,t,r){let n=Eee(e,t,r);switch(n[0]){case I$:case T$:return n[1];case C$:{let o=n[1],s=n[2];if(s.kty===\"oct\")return nd(s.k);if(!Object.isFrozen(o)){let{key_ops:a}=o;Array.isArray(a)&&Object.freeze(a),Object.freeze(o)}return E$(o,s,e)}case w$:{let o=n[1];return o.type===\"secret\"?o.export():\"toCryptoKey\"in o&&typeof o.toCryptoKey==\"function\"?Tee(o,e):E$(o,o.export({format:\"jwk\"}),e)}}}function k$(e){let t={__proto__:null};for(let r in e)t[r]={...e[r],alg:r};return t}var v$={__proto__:null,b64:!0};function O$(e,t){if(t!==void 0&&(!Array.isArray(t)||t.some(r=>typeof r!=\"string\")))throw new TypeError(`\"${e}\" option must be an array of strings`);if(t)return new Set(t)}function _$(e,t,r,n,o){if(o.crit!==void 0&&n?.crit===void 0)throw new e('\"crit\" (Critical) Header Parameter MUST be integrity protected');if(!n||n.crit===void 0)return[];if(!Array.isArray(n.crit)||n.crit.length===0||n.crit.some(a=>typeof a!=\"string\"||a.length===0))throw new e('\"crit\" (Critical) Header Parameter MUST be an array of non-empty strings when present');let s=r===void 0?t:{__proto__:null,...r,...t};for(let a of n.crit){if(!(a in s))throw new bo(`Extension Header Parameter \"${a}\" is not recognized`);if(!Object.hasOwn(o,a)||o[a]===void 0)throw new e(`Extension Header Parameter \"${a}\" is missing`);if(s[a]&&(!Object.hasOwn(n,a)||n[a]===void 0))throw new e(`Extension Header Parameter \"${a}\" MUST be integrity protected`)}return n.crit}function R$(e,t){if(t.includes(\"b64\")){let r=e.b64;if(typeof r!=\"boolean\")throw new Gr('The \"b64\" (base64url-encode payload) Header Parameter must be a boolean');return r}return!0}async function wee(e,t,r){return t instanceof Uint8Array?crypto.subtle.importKey(\"raw\",t,e.subtle,!1,[r]):(x$(t,e.subtle,r),e.minRsaBits&&h$(e.alg,t),t)}async function P$(e,t,r,n){let o=await wee(e,t,\"verify\");try{return await crypto.subtle.verify(e.signing,o,r,n)}catch{return!1}}var od=[[\"verify\"],[\"sign\"]];function H0(e){let t={name:\"HMAC\",hash:`SHA-${e}`};return{kty:[\"oct\"],secret:!0,subtle:t,signing:t,usages:od}}function df(e,t){let n={name:t?\"RSA-PSS\":\"RSASSA-PKCS1-v1_5\",hash:`SHA-${e}`};return{kty:[\"RSA\"],subtle:n,signing:t?{...n,saltLength:t}:n,usages:od,minRsaBits:2048}}function W0(e,t){return{kty:[\"EC\"],crv:e,subtle:{name:\"ECDSA\",namedCurve:e},signing:{name:\"ECDSA\",hash:`SHA-${t}`},usages:od}}function M$(){let e={name:\"Ed25519\"};return{kty:[\"OKP\"],crv:\"Ed25519\",subtle:e,signing:e,usages:od}}function J0(e){let r={name:`ML-DSA-${e}`};return{kty:[\"AKP\"],subtle:r,signing:r,usages:od}}var K0=k$({HS256:H0(256),HS384:H0(384),HS512:H0(512),RS256:df(256),RS384:df(384),RS512:df(512),PS256:df(256,32),PS384:df(384,48),PS512:df(512,64),ES256:W0(\"P-256\",256),ES384:W0(\"P-384\",384),ES512:W0(\"P-521\",512),EdDSA:M$(),Ed25519:M$(),\"ML-DSA-44\":J0(44),\"ML-DSA-65\":J0(65),\"ML-DSA-87\":J0(87)});function F$(e){let t=typeof e==\"string\"?K0[e]:void 0;if(!t)throw new bo(`alg ${e} is not supported either by JOSE or your javascript runtime`);return t}function N$(e){return[e&&O$(\"algorithms\",e.algorithms),e?.crit]}function Cee(e,t=e===void 0?{}:Fg(e,Gr,\"JWS Protected Header is invalid\")){return t}function Aee(e,t,r){let n=R$(e,_$(Gr,v$,r[1],e,t)),o=t.alg;if(typeof o!=\"string\"||!o)throw new Gr('JWS \"alg\" (Algorithm) Header Parameter missing or invalid');if(r[0]&&!r[0].has(o))throw new Og('\"alg\" (Algorithm) Header Parameter value not allowed');return[n,o]}function kee(e){try{return ff(e)}catch{throw new Gr(\"JWS Compact Serialization payload must use only ASCII characters\")}}async function vee(e,t,r,n,o,s,a){let i=!1;typeof r==\"function\"&&(r=await r(o,e),i=!0);let c=typeof a==\"string\",u=F$(s),f=m$(n!==void 0?ff(n):new Uint8Array,ff(\".\"),c?t[2]??=S$(a,\"payload\",Gr):a),p=B0(e.signature,\"signature\",Gr),d=await A$(u,r,\"verify\");if(!await P$(u,d,p,f))throw new Pg;return[c?B0(a,\"payload\",Gr):a,o,c,d,i]}async function U$(e,t,r){if(e instanceof Uint8Array&&(e=Xp.decode(e)),typeof e!=\"string\")throw new Gr(\"Compact JWS must be a string or Uint8Array\");let{0:n,1:o,2:s,length:a}=e.split(\".\");if(a!==3)throw new Gr(\"Invalid Compact JWS\");let i={payload:o,protected:n,signature:s},c=Cee(n),[u,f]=Aee(c,c,t),p=u?o:kee(o);return vee(i,t,r,n,c,f,p)}var Oee=e=>Math.floor(e.getTime()/1e3),_ee={s:1,m:60,h:3600,d:86400,w:604800,y:31557600},Ree=/^(\\+|\\-)? ?(\\d+|\\d+\\.\\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i,mf=\"check_failed\";function G0(){throw new TypeError(\"Invalid time period format\")}function D$(e){typeof e!=\"string\"&&G0();let t=Ree.exec(e);(!t||t[4]&&t[1])&&G0();let r=parseFloat(t[2]),n=Math.round(r*_ee[t[3][0].toLowerCase()]);return Number.isFinite(n)||G0(),t[1]===\"-\"||t[4]===\"ago\"?-n:n}function V0(e,t){if(!Number.isFinite(t))throw new TypeError(`Invalid ${e} input`);return t}var L$=e=>{let t=e.toLowerCase();return e.includes(\"/\")?t:`application/${t}`},Pee=(e,t)=>typeof e==\"string\"?t.includes(e):Array.isArray(e)?t.some(r=>e.includes(r)):!1;function Z0(e,t,r=!1){let n=e[t];if(!(n===void 0&&!r)){if(typeof n!=\"number\")throw new Us(`\"${t}\" claim must be a number`,e,t,\"invalid\");return n}}function Y0(e,t){throw new Us(`unexpected \"${t}\" claim value`,e,t,mf)}function $$(e,t,r={}){let n;try{n=JSON.parse(kg.decode(t))}catch{}if(!va(n))throw new lf(\"JWT Claims Set must be a top-level JSON object\");let{typ:o}=r;if(o!==void 0&&(typeof e.typ!=\"string\"||L$(e.typ)!==L$(o)))throw new Us('unexpected \"typ\" JWT header value',n,\"typ\",mf);let{requiredClaims:s=[],issuer:a,subject:i,audience:c,maxTokenAge:u}=r,f=[...s];u!==void 0&&f.push(\"iat\"),c!==void 0&&f.push(\"aud\"),i!==void 0&&f.push(\"sub\"),a!==void 0&&f.push(\"iss\");for(let E of new Set(f.reverse()))if(!Object.hasOwn(n,E))throw new Us(`missing required \"${E}\" claim`,n,E,\"missing\");a!==void 0&&!(Array.isArray(a)?a:[a]).includes(n.iss)&&Y0(n,\"iss\"),i!==void 0&&n.sub!==i&&Y0(n,\"sub\"),c!==void 0&&!Pee(n.aud,typeof c==\"string\"?[c]:c)&&Y0(n,\"aud\");let{clockTolerance:p}=r,d=0;if(typeof p==\"string\")d=D$(p);else if(p!==void 0){if(typeof p!=\"number\")throw new TypeError(\"Invalid clockTolerance option type\");d=p}V0(\"clockTolerance option\",d);let{currentDate:m}=r,x=V0(\"currentDate option\",Oee(m===void 0?new Date:m)),y=Z0(n,\"iat\",u!==void 0),S=Z0(n,\"nbf\");if(S!==void 0&&S>x+d)throw new Us('\"nbf\" claim timestamp check failed',n,\"nbf\",mf);let w=Z0(n,\"exp\");if(w!==void 0&&w<=x-d)throw new td('\"exp\" claim timestamp check failed',n,\"exp\",mf);if(u!==void 0){let E=x-y,U=V0(\"maxTokenAge option\",typeof u==\"number\"?u:D$(u));if(E-d>U)throw new td('\"iat\" claim timestamp check failed (too far in the past)',n,\"iat\",mf);if(E<-d)throw new Us('\"iat\" claim timestamp check failed (it should be in the past)',n,\"iat\",mf)}return n}async function Dg(e,t,r){let n=await U$(e,N$(r),t);if(!n[2])throw new lf(\"JWTs MUST NOT use unencoded payload\");let s={payload:$$(n[1],n[0],r),protectedHeader:n[1]};return typeof t==\"function\"?{...s,key:n[3]}:s}function Mee(e,t,r,n){let{kty:o,key_ops:s,ext:a,kid:i,alg:c,use:u,crv:f}=j0(e),p=Array.isArray(s)?[...s]:s;return(a===void 0||typeof a==\"boolean\")&&(p===void 0||Array.isArray(p)&&p.every((d,m)=>typeof d==\"string\"&&p.indexOf(d)===m)&&p.includes(\"verify\"))&&t.kty.includes(o)&&(n===void 0||typeof n==\"string\"&&n===i)&&(c===void 0?o!==\"AKP\":r===c)&&(u===void 0||u===\"sig\")&&(!t.crv||f===t.crv)}async function B$(e,t,r){let n=e.get(t)||e.set(t,{}).get(t),{alg:o}=r;if(n[o]===void 0){let s=await Ng(r,{...t,alg:o,ext:!0});if(s.type!==\"public\")throw new rd(\"JSON Web Key Set members must be public keys\");n[o]=s}return n[o]}function uc(e){let t;try{t=structuredClone(e)}catch{}if(!Mg(t))throw new rd(\"JSON Web Key Set malformed\");let r=new WeakMap;return Object.defineProperty(async(o,s)=>{let{alg:a,kid:i}={...o,...s?.header},c=typeof a==\"string\"?K0[a]:void 0;if(!c||c.secret)throw new bo('Unsupported \"alg\" value for a JSON Web Key Set');let u=t.keys.filter(d=>Mee(d,c,a,i)),{0:f,length:p}=u;if(!p)throw new pf;if(p!==1){let d=new _g;throw d[Symbol.asyncIterator]=async function*(){for(let m of u)try{yield await B$(r,m,c)}catch{}},d}return B$(r,f,c)},\"jwks\",{value:()=>structuredClone(t)})}function Fee(){return typeof WebSocketPair<\"u\"||typeof navigator<\"u\"&&navigator.userAgent===\"Cloudflare-Workers\"||typeof EdgeRuntime<\"u\"&&EdgeRuntime===\"vercel\"}var Q0;(typeof navigator>\"u\"||!navigator.userAgent?.startsWith?.(\"Mozilla/5.0 \"))&&(Q0=\"jose/v6.2.10\");var z$=Symbol();async function Nee(e,t,r,n=fetch){let o=await n(e,{method:\"GET\",signal:r,redirect:\"manual\",headers:t}).catch(s=>{throw s.name===\"TimeoutError\"?new Rg:s});if(o.status!==200)throw new yr(\"Expected 200 OK from the JSON Web Key Set HTTP response\");try{return await o.json()}catch{throw new yr(\"Failed to parse the JSON Web Key Set HTTP response as JSON\")}}var q$=Symbol();function sd(e,t){return Number.isFinite(e)&&Date.now(){if(d&&Fee()&&(d=void 0),!d){let E=++m,U=d=Nee(r,c,AbortSignal.timeout(s),u).then(_=>{let h=uc(_);if(E<=x)return;y=h;let b=Date.now();f&&(f.uat=b,f.jwks=_),p=b,x=E}).finally(()=>{d===U&&(d=void 0)})}await d};return Object.defineProperties(async(E,U)=>{(!y||!sd(p,i))&&await S();try{return await y(E,U)}catch(_){if(_ instanceof pf&&!sd(p,a))return await S(),y(E,U);throw _}},{coolingDown:{get:()=>sd(p,a),enumerable:!0},fresh:{get:()=>sd(p,i),enumerable:!0},reload:{value:S,enumerable:!0},reloading:{get:()=>!!d,enumerable:!0},jwks:{value:()=>y?.jwks(),enumerable:!0}})}function eC(e){let t;if(typeof e==\"string\"){let n=e.split(\".\");(n.length===3||n.length===5)&&([t]=n)}else if(typeof e==\"object\"&&e)if(\"protected\"in e)t=e.protected;else throw new TypeError(\"Token does not contain a Protected Header\");let r=\"Invalid Token or Protected Header formatting\";if(typeof t!=\"string\"||!t)throw new TypeError(r);return Fg(t,TypeError,r)}var Dee=new Set([\"HOME\",\"HOSTNAME\",\"PATH\",\"PWD\"]),Ln=N.runSync(N.gen(function*(){let e=t=>t.pipe(Mr.option);return{hostPort:yield*e(Mr.String(\"SUPABASE_INTERNAL_HOST_PORT\")),functionsRoot:yield*e(Mr.String(\"SUPABASE_INTERNAL_FUNCTIONS_ROOT\")),filesRoot:yield*e(Mr.String(\"SUPABASE_INTERNAL_FUNCTIONS_FILES_ROOT\")),jwtSecret:yield*e(Mr.String(\"SUPABASE_INTERNAL_JWT_SECRET\")),supabaseUrl:yield*e(Mr.String(\"SUPABASE_URL\")),wallclock:yield*e(Mr.String(\"SUPABASE_INTERNAL_WALLCLOCK_LIMIT_SEC\")),publishableKey:yield*e(Mr.String(\"SUPABASE_INTERNAL_PUBLISHABLE_KEY\")),secretKey:yield*e(Mr.String(\"SUPABASE_INTERNAL_SECRET_KEY\")),functionsConfig:yield*e(Mr.String(\"SUPABASE_INTERNAL_FUNCTIONS_CONFIG\")),debug:yield*e(Mr.String(\"SUPABASE_INTERNAL_DEBUG\")),jwks:yield*e(Mr.String(\"SUPABASE_JWKS\"))}}).pipe(N.provideService(sf.ConfigProvider,sf.fromEnvRecord(Deno.env.toObject(),{preserveEmptyStrings:!0})))),xf=(e,t=\"\")=>xe.getOrElse(e,()=>t),H$=xf(Ln.hostPort,\"8081\"),Z$=xf(Ln.functionsRoot),Lee=xf(Ln.jwtSecret),$ee=xf(Ln.supabaseUrl,\"http://127.0.0.1:54321\"),Bee=new URL(\"/auth/v1/.well-known/jwks.json\",$ee),W$=Number.parseInt(xf(Ln.wallclock,\"400\"),10),J$=xe.getOrUndefined(Ln.publishableKey),K$=xe.getOrUndefined(Ln.secretKey),Ds={BootError:Kr.ServiceUnavailable,InvalidWorkerResponse:Kr.InternalServerError,WorkerLimit:546},jee={[Ds.BootError]:\"BOOT_ERROR\",[Ds.InvalidWorkerResponse]:\"WORKER_ERROR\",[Ds.WorkerLimit]:\"WORKER_LIMIT\"},zee={[Ds.BootError]:\"Worker failed to boot (please check logs)\",[Ds.InvalidWorkerResponse]:\"Function exited due to an error (please check logs)\",[Ds.WorkerLimit]:\"Worker failed to respond due to a resource limit (please check logs)\"},qee=new Map([[Deno.errors.InvalidWorkerCreation,Ds.BootError],[Deno.errors.InvalidWorkerResponse,Ds.InvalidWorkerResponse],[Deno.errors.WorkerRequestCancelled,Ds.WorkerLimit]]),Hee=e=>{let t=Deno.errors.WorkerAlreadyRetired;return t!==void 0&&e instanceof t},Wee=(s=>(s.MissingAuthHeader=\"UNAUTHORIZED_NO_AUTH_HEADER\",s.InvalidLegacyJWT=\"UNAUTHORIZED_JWT\",s.InvalidAsymmetricJWT=\"UNAUTHORIZED_ASYMMETRIC_JWT\",s.InvalidTokenFormat=\"UNAUTHORIZED_INVALID_JWT_FORMAT\",s.UnsupportedTokenAlgorithm=\"UNAUTHORIZED_UNSUPPORTED_TOKEN_ALGORITHM\",s))(Wee||{}),Jee=me.Struct({enabled:me.optionalKey(me.Boolean),verifyJWT:me.optionalKey(me.Boolean),verify_jwt:me.optionalKey(me.Boolean),entrypointPath:me.optionalKey(me.String),entrypoint:me.optionalKey(me.String),importMapPath:me.optionalKey(me.String),import_map:me.optionalKey(me.String),importMapRoot:me.optionalKey(me.String),import_map_root:me.optionalKey(me.String),staticFiles:me.optionalKey(me.Array(me.String)),static_files:me.optionalKey(me.Array(me.String)),env:me.optionalKey(me.Record(me.String,me.String))}),Kee=me.Record(me.String,Jee),Gee=me.declare(e=>typeof e==\"object\"&&e!==null&&\"keys\"in e&&Array.isArray(e.keys)&&e.keys.every(t=>typeof t==\"object\"&&t!==null)),Vee=()=>xe.match(Ln.functionsConfig,{onNone:()=>({}),onSome:e=>N.runSync(me.decodeEffect(me.fromJsonString(Kee))(e).pipe(N.orElseSucceed(()=>({}))))}),tC=Vee();if(xe.getOrUndefined(Ln.debug)===\"true\"){let e=Object.fromEntries(Object.entries(tC).map(([t,r])=>[t,Object.fromEntries(Object.entries(r).filter(([n])=>n!==\"env\"))]));N.runSync(ic.log(\"Functions config:\",JSON.stringify(e,null,2)))}var fc=(e,t,r={})=>{let n={...r},o=null;return e!=null&&(typeof e==\"object\"?(n[\"Content-Type\"]=\"application/json\",o=JSON.stringify(e)):(n[\"Content-Type\"]=\"text/plain\",o=typeof e==\"string\"?e:JSON.stringify(e)??null)),new Response(o,{status:t,headers:n})},G$=({code:e,message:t=\"Invalid JWT\"})=>fc({code:e,message:t,msg:t},Kr.Unauthorized,{\"sb-error-code\":e,\"Access-Control-Expose-Headers\":\"sb-error-code\"}),Zee=e=>{for(let[t,r]of qee.entries())if(t!==void 0&&e instanceof t)return fc({code:jee[r],message:zee[r]},r);return fc({code:N0[Kr.InternalServerError],message:\"Request failed due to an internal server error\"},Kr.InternalServerError)};function Yee(e){let t=e.split(\" \");return t.length===2&&t[0]===\"Bearer\"?t[1]:null}var Qee=e=>{let t=e.headers.get(\"authorization\"),r=e.headers.get(\"sb-api-key\")?.replace(\"Bearer\",\"\").trim();if(!t&&!r)return{code:\"UNAUTHORIZED_NO_AUTH_HEADER\",message:\"Missing authorization header\"};let n=Yee(t??\"\"),o=!n||n.startsWith(\"sb_\")?r:n;return o||{code:\"UNAUTHORIZED_INVALID_JWT_FORMAT\",message:\"Invalid JWT format\"}},hf=class extends Cn.TaggedError(\"BootstrapOperationError\"){},Xee=N.runSync(xe.match(Ln.jwks,{onNone:()=>N.succeed(xe.some(uc({keys:[]}))),onSome:e=>N.gen(function*(){let t=yield*me.decodeEffect(me.fromJsonString(Gee))(e);return yield*N.try({try:()=>uc(t),catch:r=>new hf({cause:r})})}).pipe(N.option)})),ete=xe.getOrElse(Xee,()=>X0(Bee)),Oa=e=>N.tryPromise({try:e,catch:t=>new hf({cause:t})}),tte=e=>N.gen(function*(){return yield*Oa(()=>Dg(e,ete)),xe.none()}).pipe(N.orElseSucceed(()=>xe.some({code:\"UNAUTHORIZED_ASYMMETRIC_JWT\"})));function rte(e,t){return N.gen(function*(){let r=yield*N.try({try:()=>eC(t).alg,catch:n=>new hf({cause:n})});return r?r===\"HS256\"?yield*Oa(()=>Dg(t,new TextEncoder().encode(e))).pipe(N.as(xe.none()),N.orElseSucceed(()=>xe.some({code:\"UNAUTHORIZED_JWT\"}))):r===\"ES256\"||r===\"RS256\"?yield*tte(t):xe.some({code:\"UNAUTHORIZED_UNSUPPORTED_TOKEN_ALGORITHM\",message:`Unsupported JWT algorithm ${r}`}):xe.some({code:\"UNAUTHORIZED_INVALID_JWT_FORMAT\",message:\"Invalid JWT format\"})}).pipe(N.orElseSucceed(()=>xe.some({code:\"UNAUTHORIZED_INVALID_JWT_FORMAT\",message:\"Invalid JWT format\"})))}var Y$={lstat:e=>Oa(()=>Deno.lstat(e)).pipe(N.mapError(t=>new uf({cause:t})),N.map(t=>({isDirectory:t.isDirectory,isFile:t.isFile,isSymbolicLink:t.isSymlink}))),realPath:e=>Oa(()=>Deno.realPath(e)).pipe(N.mapError(t=>new uf({cause:t}))),readDirectory:e=>ns.suspend(()=>ns.fromAsyncIterable(Deno.readDir(e),t=>new hf({cause:t})).pipe(ns.map(t=>t.name))).pipe(ns.runCollect,N.mapError(t=>new uf({cause:t})))},nte=e=>l$({root:Z$,filesRoot:xe.getOrUndefined(Ln.filesRoot),slug:e,overrides:tC,fs:Y$}),V$=new Set,ote=(e,t)=>t.importMapDiscoveredByRuntime||!f$(t.importMapPath)||V$.has(e)?N.void:N.sync(()=>V$.add(e)).pipe(N.andThen(ic.warn(`[functions] ${e}: ${t.importMapPath} is not the nearest Deno config of ${t.entrypointPath}, so Edge Runtime loads it as a plain import map without comments or jsr:/npm: subpath imports. Move it next to the entrypoint or into a parent directory without a closer deno.json(c).`))),ste=p$(()=>Deno.makeTempDirSync({prefix:\"supabase-worker-\"})),ate=e=>e.importMapPath?N.succeed(!1):d$({root:xf(Ln.filesRoot,Z$),config:e,fs:Y$}),Q$=e=>ns.fromEffectRepeat(Oa(()=>e.read()).pipe(N.flatMap(t=>t.done?Ft.done():N.succeed(t.value)))),ite=e=>e===void 0?N.void:ns.runDrain(Q$(e)).pipe(N.ignore);function cte(e,t){let r=new URL(e.url),n=e.headers.get(\"x-forwarded-host\");n&&(r.hostname=n);let o=new Request(r.href,{method:e.method,headers:e.headers,body:t===void 0?null:ns.toReadableStream(Q$(t)),signal:e.signal,duplex:\"half\"});return o.headers.delete(\"sb-api-key\"),EdgeRuntime.applySupabaseTag(e,o),o}Deno.serve({handler:e=>N.runPromiseExit(N.gen(function*(){let t=yield*N.acquireRelease(N.sync(()=>e.body?.getReader()),u=>N.interruptible(ite(u))),{pathname:r}=new URL(e.url);if(r===\"/_internal/health\")return fc({message:\"ok\"},Kr.OK);if(r===\"/_internal/metric\")return Response.json(yield*Oa(()=>EdgeRuntime.getRuntimeMetrics()));let n=r.split(\"/\")[1];if(!n)return fc(\"Function not found\",Kr.NotFound);let o=yield*nte(n);if(!o)return fc(\"Function not found\",Kr.NotFound);if(yield*ote(n,o),e.method!==\"OPTIONS\"&&o.verifyJWT){let u=Qee(e);if(typeof u!=\"string\")return G$(u);let f=yield*rte(Lee,u);if(xe.isSome(f))return G$(f.value)}let s={...Deno.env.toObject(),...Object.fromEntries(Object.entries(o.env??{}).filter(([u])=>!u.startsWith(\"SUPABASE_\"))),SUPABASE_FUNCTION_SLUG:n};J$&&(s.SUPABASE_PUBLISHABLE_KEYS=yield*me.encodeEffect(me.fromJsonString(me.Unknown))({default:J$})),K$&&(s.SUPABASE_SECRET_KEYS=yield*me.encodeEffect(me.fromJsonString(me.Unknown))({default:K$}));let a=Object.entries(s).filter(([u])=>!Dee.has(u)&&!u.startsWith(\"SUPABASE_INTERNAL_\")),i=!(yield*ate(o));return yield*N.gen(function*(){let u=yield*Oa(()=>EdgeRuntime.userWorkers.create({servicePath:ste(n,o),memoryLimitMb:256,workerTimeoutMs:Number.isFinite(W$)?W$*1e3:4e5,noModuleCache:!0,noNpm:i,envVars:a,forceCreate:!0,customModuleRoot:\"\",cpuTimeSoftLimitMs:1e3,cpuTimeHardLimitMs:2e3,decoratorType:\"tc39\",maybeEntrypoint:i$(o.entrypointPath).href,context:{useReadSyncFileAPI:!0,...o.importMapPath===\"\"||o.importMapDiscoveredByRuntime?{}:{importMapPath:o.importMapPath}},staticPatterns:o.staticFiles}));return yield*Oa(()=>u.fetch(cte(e,t)))}).pipe(N.retry({times:1,while:({cause:u})=>e.body===null&&Hee(u)}),N.catchTag(\"BootstrapOperationError\",({cause:u})=>ic.error(\"[functions] worker error\",u).pipe(N.andThen(N.succeed(Zee(u))))))}).pipe(N.scoped),{signal:e.signal}).then(t=>{if(ir.isSuccess(t))return t.value;if(e.signal.aborted&&Ft.hasInterruptsOnly(t.cause))return new Response(null,{status:499});throw Ft.squash(t.cause)}),onListen:()=>{let t=Object.keys(tC).slice(0,5).map(r=>` - http://127.0.0.1:${H$}/functions/v1/${r}`);N.runSync(ic.log(`Serving functions on http://127.0.0.1:${H$}/functions/v1/${t.length?`\n${t.join(`\n`)}`:\"\"}\nUsing ${Deno.version.deno}`))},onError:()=>fc({code:N0[500],message:\"Request failed due to an internal server error\"},500)});export{Wee as RequestErrors,Yee as extractBearerToken,cte as prepareUserRequest};\n"; +export const defaultFunctionsBootstrap = "var eB=Object.defineProperty;var Eo=(e,t)=>{for(var r in t)eB(e,r,{get:t[r],enumerable:!0})};var Z=(e,t)=>{switch(t.length){case 0:return e;case 1:return t[0](e);case 2:return t[1](t[0](e));case 3:return t[2](t[1](t[0](e)));case 4:return t[3](t[2](t[1](t[0](e))));case 5:return t[4](t[3](t[2](t[1](t[0](e)))));case 6:return t[5](t[4](t[3](t[2](t[1](t[0](e))))));case 7:return t[6](t[5](t[4](t[3](t[2](t[1](t[0](e)))))));case 8:return t[7](t[6](t[5](t[4](t[3](t[2](t[1](t[0](e))))))));case 9:return t[8](t[7](t[6](t[5](t[4](t[3](t[2](t[1](t[0](e)))))))));default:{let r=e;for(let n=0,o=t.length;nt(r,...arguments)};switch(e){case 0:case 1:throw new RangeError(`Invalid arity ${e}`);case 2:return function(r,n){return arguments.length>=2?t(r,n):function(o){return t(o,r)}};case 3:return function(r,n,o){return arguments.length>=3?t(r,n,o):function(s){return t(s,r,n)}};default:return function(){if(arguments.length>=e)return t.apply(this,arguments);let r=arguments;return function(n){return t(n,...r)}}}};var R=e=>e;var je=e=>()=>e,Sr=je(!0),yf=je(!1),$g=je(null),bn=je(void 0),$n=bn;function Bg(e,...t){return Z(e,t)}function rC(e,t,r,n,o,s,a,i,c){switch(arguments.length){case 1:return e;case 2:return function(){return t(e.apply(this,arguments))};case 3:return function(){return r(t(e.apply(this,arguments)))};case 4:return function(){return n(r(t(e.apply(this,arguments))))};case 5:return function(){return o(n(r(t(e.apply(this,arguments)))))};case 6:return function(){return s(o(n(r(t(e.apply(this,arguments))))))};case 7:return function(){return a(s(o(n(r(t(e.apply(this,arguments)))))))};case 8:return function(){return i(a(s(o(n(r(t(e.apply(this,arguments))))))))};case 9:return function(){return c(i(a(s(o(n(r(t(e.apply(this,arguments)))))))))}}}function Vr(e){let t=new WeakMap;return r=>{let n=t.get(r);if(n!==void 0)return n;let o=e(r);return t.set(r,o),o}}function Pa(e){let t=new WeakMap;return r=>{let n=t.get(r);if(n!==void 0)return n;let o=e(r);return t.set(r,o),o!==r&&t.set(o,o),o}}var Ft={};Eo(Ft,{AsyncFiberError:()=>vq,AsyncFiberErrorTypeId:()=>Aq,Done:()=>ub,DoneTypeId:()=>yq,ExceededCapacityError:()=>fb,ExceededCapacityErrorTypeId:()=>Cq,IllegalArgumentError:()=>ml,IllegalArgumentErrorTypeId:()=>Iq,InterruptorStackTrace:()=>XS,NoSuchElementError:()=>gq,NoSuchElementErrorTypeId:()=>xq,ReasonTypeId:()=>tq,StackTrace:()=>QS,TimeoutError:()=>Eq,TimeoutErrorTypeId:()=>Sq,TypeId:()=>eq,UnknownError:()=>Rq,UnknownErrorTypeId:()=>Oq,annotate:()=>Pq,annotations:()=>Mq,combine:()=>aq,die:()=>ci,done:()=>Y,empty:()=>nq,fail:()=>oq,filterInterruptors:()=>dq,findDefect:()=>dl,findDie:()=>uq,findError:()=>ab,findErrorOption:()=>ib,findFail:()=>iq,findInterrupt:()=>lq,fromReasons:()=>oa,hasDies:()=>cq,hasFails:()=>sb,hasInterrupts:()=>fq,hasInterruptsOnly:()=>ob,interrupt:()=>sq,interruptors:()=>pq,isAsyncFiberError:()=>kq,isCause:()=>eb,isDieReason:()=>rb,isDone:()=>qc,isExceededCapacityError:()=>wq,isFailReason:()=>na,isIllegalArgumentError:()=>Tq,isInterruptReason:()=>rq,isNoSuchElementError:()=>hq,isReason:()=>tb,isTimeoutError:()=>bq,isUnknownError:()=>_q,makeDieReason:()=>zc,makeFailReason:()=>jc,makeInterruptReason:()=>nb,map:()=>Ss,pretty:()=>cb,prettyErrors:()=>mq,reasonAnnotations:()=>Mm,squash:()=>Pm});var Sf=e=>{let t=new Set(Reflect.ownKeys(e));if(e.constructor===Object)return t;e instanceof Error&&t.delete(\"stack\");let r=Object.getPrototypeOf(e),n=r;for(;n!==null&&n!==Object.prototype;){let o=Reflect.ownKeys(n);for(let s=0;snew Uint8Array(e.buffer,e.byteOffset,e.byteLength);var Ma=0,oC=()=>{Ma++};function br(e){return typeof e==\"string\"}function Ef(e){return typeof e==\"number\"}function ad(e){return typeof e==\"boolean\"}function sC(e){return typeof e==\"bigint\"}function jg(e){return typeof e==\"symbol\"}function id(e){return br(e)||Ef(e)||jg(e)}function Zr(e){return typeof e==\"function\"}function aC(e){return e===void 0}function Io(e){return e!==void 0}function cd(e){return e!=null}function iC(e){return!1}function zg(e){return!0}function fr(e){return typeof e==\"object\"&&e!==null&&!Array.isArray(e)}function If(e){return typeof e==\"object\"&&e!==null||Zr(e)}var k=l(2,(e,t)=>If(e)&&t in e),At=l(2,(e,t)=>k(e,\"_tag\")&&e._tag===t);function cC(e){return e instanceof Error}function as(e){return k(e,Symbol.iterator)||br(e)}var se=\"~effect/Hash\",H=e=>{switch(typeof e){case\"number\":return It(e);case\"bigint\":return Re(e.toString(10));case\"string\":return Re(e);case\"function\":case\"object\":{if(e===null)break;if(e instanceof Date)return Number.isNaN(e.getTime())?Re(\"Invalid Date\"):Re(e.toISOString());if(e instanceof RegExp)return Re(e.toString());{if(lc.has(e))return ud(e);let t=fC.get(e);if(t!==void 0)return t;if(Wg.has(e))return oC(),Re(\"[Circular]\");Wg.add(e);let r=Ma,n;try{se in e?n=e[se]():typeof e==\"function\"?n=ud(e):e instanceof DataView?n=To(bf(e)):Array.isArray(e)||ArrayBuffer.isView(e)?n=To(e):e instanceof Map?n=tB(e):e instanceof Set?n=rB(e):n=Tf(e)}finally{Wg.delete(e)}return r===Ma&&fC.set(e,n),n}}}return qt(lC(Re(String(e))))},ud=e=>(Hg.has(e)||Hg.set(e,qt(Math.random()*4294967296|0)),Hg.get(e)),lC=e=>(e^=e>>>16,e=Math.imul(e,2246822507),e^=e>>>13,e=Math.imul(e,3266489909),e^e>>>16),le=l(2,(e,t)=>lC(Math.imul(e,2654435761)+Math.imul(t,2246822507))),qt=e=>e&3221225471|e>>>1&1073741824;var qg=new DataView(new ArrayBuffer(8)),It=e=>{let t=e|0;return t===e?qt(t):(qg.setFloat64(0,e!==e?NaN:e),qt(le(qg.getInt32(0),qg.getInt32(4))))},Re=e=>{let t=5381,r=e.length;for(;r;)t=t*33^e.charCodeAt(--r);return qt(t)},Jg=(e,t)=>{let r=12289;for(let n of t)r^=le(H(n),H(e[n]));return qt(r)},Tf=e=>Jg(e,Sf(e)),pC=(e,t)=>r=>{let n=e;for(let o of r)n^=t(o);return qt(n)},To=e=>{let t=6151;for(let r of e)t=le(t,H(r));return qt(t)},tB=pC(Re(\"Map\"),([e,t])=>le(H(e),H(t))),uC=Re(\"Set\"),rB=pC(uC,e=>le(uC,H(e))),Hg=new WeakMap,fC=new WeakMap,Wg=new WeakSet;var ae=\"~effect/Equal\";function J(){return arguments.length===1?e=>Fa(e,arguments[0]):Fa(arguments[0],arguments[1])}function Fa(e,t){if(e===t)return!0;if(e==null||t==null)return!1;let r=typeof e;return r!==typeof t?!1:r===\"number\"&&e!==e&&t!==t?!0:r!==\"object\"&&r!==\"function\"||lc.has(e)||lc.has(t)?!1:nB(e,t)}function nB(e,t){let r=fd.length;for(let s=r;s-- >0;)if(fd[s]===e&&Kg[s]===t)return!0;if(r)return dC(e,t);let n=mC.get(e);n||mC.set(e,n=new WeakMap);let o=n.get(t);return o===void 0&&n.set(t,o=dC(e,t)),o}function dC(e,t){fd.push(e),Kg.push(t);try{return oB(e,t)}finally{fd.pop(),Kg.pop()}}var fd=[],Kg=[],mC=new WeakMap;function oB(e,t){if(H(e)!==H(t))return!1;if(e instanceof Date){if(!(t instanceof Date))return!1;let n=e.getTime(),o=t.getTime();return n===o||Number.isNaN(n)&&Number.isNaN(o)}else if(e instanceof RegExp)return t instanceof RegExp&&e.toString()===t.toString();let r=xC(e);if(r!==xC(t)||typeof e==\"function\"&&!r)return!1;if(r)return e[ae](t);if(Array.isArray(e))return!Array.isArray(t)||e.length!==t.length?!1:sB(e,t);if(ArrayBuffer.isView(e)){let n=e instanceof DataView;return!ArrayBuffer.isView(t)||e.byteLength!==t.byteLength||n!==t instanceof DataView?!1:n?hC(bf(e),bf(t)):hC(e,t)}else{if(e instanceof Map)return!(t instanceof Map)||e.size!==t.size?!1:Gg(e,t,iB,cB);if(e instanceof Set)return!(t instanceof Set)||e.size!==t.size?!1:Gg(e,t,H,Fa)}return aB(e,t)}function sB(e,t){for(let r=0;rH(e[0]),cB=(e,t)=>Fa(e[0],t[0])&&Fa(e[1],t[1]),uB=()=>0;function ld(e,t){return wf((r,n)=>e(r[0],n[0])&&t(r[1],n[1]))}function wf(e){return function(r,n){return Gg(r,n,uB,e)}}var xC=e=>k(e,ae),Vg=()=>J;var gC=e=>(lc.add(e),e);var Na=Symbol.for(\"~effect/Redactable\"),fB=e=>k(e,Na);function is(e){return fB(e)?Zg(e):e}function Zg(e){return e[Na](globalThis[pc]?.context??lB)}var pc=\"~effect/Fiber/currentFiber\",yC=new Map,lB={\"~effect/Context\":{},base:yC,depth:0,mapUnsafe:yC,pipe(){return Z(this,arguments)}};function F(e,t){let r=t?.space??0,n=new WeakSet,o=r?typeof r==\"number\"?\" \".repeat(r):r:\"\",s=f=>o.repeat(f),a=(f,p)=>{let d=f?.constructor;return d&&d!==Object.prototype.constructor&&d.name?`${d.name}(${p})`:p},i=f=>{try{return Reflect.ownKeys(f)}catch{return[\"[ownKeys threw]\"]}};function c(f,p=0){try{return u(f,p)}catch{return(typeof f==\"object\"&&f!==null||typeof f==\"function\")&&n.delete(f),\"[inspection threw]\"}}function u(f,p=0){if(typeof f==\"string\")return JSON.stringify(f);if(typeof f==\"number\"||f==null||typeof f==\"boolean\"||typeof f==\"symbol\")return String(f);if(typeof f==\"bigint\")return String(f)+\"n\";if(typeof f==\"object\"||typeof f==\"function\"){if(n.has(f))return pB;n.add(f);let d;if(Na in f)d=c(Zg(f),p);else if(Array.isArray(f))d=!o||f.length<=1?`[${f.map(m=>c(m,p)).join(\",\")}]`:`[\n${s(p+1)}${f.map(m=>c(m,p+1)).join(`,\n`+s(p+1))}\n${s(p)}]`;else if(f instanceof Date)d=Yg(f);else if(!t?.ignoreToString&&k(f,\"toString\")&&typeof f.toString==\"function\"&&f.toString!==Object.prototype.toString&&f.toString!==Array.prototype.toString){let m=dB(f);d=f instanceof Error&&f.cause!==void 0?`${m} (cause: ${c(f.cause,p)})`:m}else if(Symbol.iterator in f)d=`${f.constructor.name}(${c(Array.from(f),p)})`;else{let m=i(f);if(!o||m.length<=1){let x=`{${m.map(y=>`${wo(y)}:${c(bC(f,y),p)}`).join(\",\")}}`;d=a(f,x)}else{let x=`{\n${m.map(y=>`${s(p+1)}${wo(y)}: ${c(bC(f,y),p+1)}`).join(`,\n`)}\n${s(p)}}`;d=a(f,x)}}return n.delete(f),d}return String(f)}return c(e,0)}var pB=\"[Circular]\";function wo(e){return typeof e==\"string\"?JSON.stringify(e):String(e)}function pd(e){return e.map(t=>`[${wo(t)}]`).join(\"\")}function Yg(e){try{return e.toISOString()}catch{return\"Invalid Date\"}}function dB(e){try{let t=e.toString();return typeof t==\"string\"?t:String(t)}catch{return\"[toString threw]\"}}function bC(e,t){try{return e[t]}catch{return\"[property access threw]\"}}function Ua(e,t){let r=[];return JSON.stringify(e,function(n,o){let s=Object.getOwnPropertyDescriptor(this,n)?.value,a=k(s,Na)?is(s):is(o);if(typeof a==\"bigint\")return F(a);if(typeof a!=\"object\"||a===null)return a;let i=a instanceof Error&&!k(a,\"toJSON\")?{...a,name:a.name,message:a.message}:a;for(;r.length>0&&r[r.length-1]!==this;)r.pop();if(r.includes(a))return;if(r.push(a),i!==a&&r.push(i),!mB(i)||hB(i))return i;let c=new Proxy(i,{get(u,f){let p=Reflect.get(u,f,u);return Object.getOwnPropertyDescriptor(u,f)?.get!==void 0?is(p):p}});return r.push(c),c},t?.space)??\"null\"}function mB(e){for(let t of Object.getOwnPropertyNames(e))if(Object.getOwnPropertyDescriptor(e,t)?.get!==void 0)return!0;return!1}function hB(e){for(let t of[Number.prototype.valueOf,Boolean.prototype.valueOf,String.prototype.valueOf])try{return Reflect.apply(t,e,[]),!0}catch{}return!1}var Te=Symbol.for(\"nodejs.util.inspect.custom\"),vt=e=>{try{return e=is(e),k(e,\"toJSON\")&&Zr(e.toJSON)&&e.toJSON.length===0?e.toJSON():Array.isArray(e)?e.map(vt):e}catch{return\"[toJSON threw]\"}},IC=(e,t=2)=>{if(typeof e==\"string\")return e;try{return typeof e==\"object\"?Ua(e,{space:t}):F(e,{space:t})}catch{return String(e)}},dc={toJSON(){return vt(this)},[Te](){return this.toJSON()},toString(){return F(this.toJSON())}},EC=class{[Te](){return this.toJSON()}toString(){return F(this.toJSON())}};var gB=()=>{let e=Object.getOwnPropertyDescriptor(Error,\"stackTraceLimit\");return e===void 0?Object.isExtensible(Error):Object.hasOwn(e,\"writable\")?e.writable===!0:e.set!==void 0},yB=gB(),Co=()=>Error.stackTraceLimit,Yr=e=>{yB&&(Error.stackTraceLimit=e)};var Bs=class{constructor(t){this.value=t,this.done=!1}next(t){return this.done?(this.value=t,this):(this.done=!0,{value:this.value,done:!1})}},SB=()=>{let e=\"~effect/Utils/internal\",t={[e]:o=>o()},r={[e]:o=>o()};return Co()!==0&&t[e](()=>new Error().stack)?.includes(e)===!0?t[e]:r[e]},js=SB();function M(e,t,r){t===\"__proto__\"?Object.defineProperty(e,t,{value:r,writable:!0,enumerable:!0,configurable:!0}):e[t]=r}function md(e,t){for(let r of Reflect.ownKeys(t))Object.prototype.propertyIsEnumerable.call(t,r)&&M(e,r,t[r])}var ko=\"~effect/Effect\",Da=\"~effect/Exit\",EB={_A:R,_E:R,_R:R},Qg=`${ko}/identifier`,ue=`${ko}/args`,Ke=`${ko}/evaluate`,_t=`${ko}/successCont`,Qr=`${ko}/failureCont`,La=`${ko}/ensureCont`,ja=Symbol.for(\"effect/Effect/Yield\"),rr={pipe(){return Z(this,arguments)},toJSON(){return{...this}},toString(){return F(this.toJSON(),{ignoreToString:!0,space:2})},[Te](){return this.toJSON()}},wC={[se](){return Jg(this,Object.keys(this))},[ae](e){let t=Object.keys(this),r=Object.keys(e);if(t.length!==r.length)return!1;for(let n=0;nk(e,ko),vf=e=>k(e,Da),Cf=\"~effect/Cause\",mc=\"~effect/Cause/Reason\",xc=e=>k(e,Cf),CC=e=>k(e,mc),Ao=class{constructor(t){this[Cf]=Cf,this.reasons=t}pipe(){return Z(this,arguments)}toJSON(){return{_id:\"Cause\",failures:this.reasons.map(t=>t.toJSON())}}toString(){return`Cause(${F(this.reasons)})`}[Te](){return this.toJSON()}[ae](t){return xc(t)&&this.reasons.length===t.reasons.length&&this.reasons.every((r,n)=>J(r,t.reasons[n]))}[se](){return To(this.reasons)}},TC=new WeakMap,hc=class{[mc];annotations;_tag;constructor(t,r,n){if(this[mc]=mc,this._tag=t,r!==Of&&typeof n==\"object\"&&n!==null&&r.size>0){let o=TC.get(n);o&&(r=new Map([...o,...r])),TC.set(n,r)}this.annotations=r}annotate(t,r){if(t.mapUnsafe.size===0)return this;let n=new Map(this.annotations);t.mapUnsafe.forEach((s,a)=>{r?.overwrite!==!0&&n.has(a)||n.set(a,s)});let o=Object.assign(Object.create(Object.getPrototypeOf(this)),this);return o.annotations=n,o}pipe(){return Z(this,arguments)}toString(){return F(this)}[Te](){return this.toString()}},Of=new Map,$a=class extends hc{constructor(t,r=Of){super(\"Fail\",r,t),this.error=t}toString(){return`Fail(${F(this.error)})`}toJSON(){return{_tag:\"Fail\",error:this.error}}[ae](t){return jn(t)&&J(this.error,t.error)&&J(this.annotations,t.annotations)}[se](){return le(Re(this._tag))(le(H(this.error))(H(this.annotations)))}},cs=e=>new Ao(e),TB=(e,t)=>{let r=new Map,n=[];for(let o of e.concat(t)){let s=H(o),a=r.get(s);if(a===void 0)r.set(s,[o]);else{if(a.some(i=>J(i,o)))continue;a.push(o)}n.push(o)}return n},za=l(2,(e,t)=>{if(e.reasons.length===0)return t;if(t.reasons.length===0)return e;let r=new Ao(TB(e.reasons,t.reasons));return J(e,r)?e:r}),hd=new Ao([]),gc=e=>new Ao([new $a(e)]),Af=class extends hc{constructor(t,r=Of){super(\"Die\",r,t),this.defect=t}toString(){return`Die(${F(this.defect)})`}toJSON(){return{_tag:\"Die\",defect:this.defect}}[ae](t){return qa(t)&&J(this.defect,t.defect)&&J(this.annotations,t.annotations)}[se](){return le(Re(this._tag))(le(H(this.defect))(H(this.annotations)))}},Xg=e=>new Ao([new Af(e)]),yc=l(e=>xc(e[0]),(e,t,r)=>t.mapUnsafe.size===0?e:new Ao(e.reasons.map(n=>n.annotate(t,r)))),jn=e=>e._tag===\"Fail\",qa=e=>e._tag===\"Die\",Ha=e=>e._tag===\"Interrupt\";function wB(e){return Er(\"Effect.evaluate: Not implemented\")}var Xr=e=>({...IB,[Qg]:e.op,[Ke]:e[Ke]??wB,[_t]:e[_t],[Qr]:e[Qr],[La]:e[La]}),En=e=>{let t=Xr(e),r=function(n){this[ue]=n};return r.prototype=t,function(n){return new r(n)}},AC=e=>{let t={[Da]:Da,_tag:e.op,get[e.prop](){return this[ue]},...Xr(e),toString(){return`${e.op}(${F(this[ue])})`},toJSON(){return{_id:\"Exit\",_tag:e.op,[e.prop]:this[ue]}},[ae](n){return vf(n)&&n._tag===this._tag&&J(this[ue],n[ue])},[se](){return le(Re(e.op),H(this[ue]))}},r=function(n){this[ue]=n};return r.prototype=t,function(n){return new r(n)}},Ae=AC({op:\"Success\",prop:\"value\",[Ke](e){let t=e.getCont(_t);return t?t[_t](this[ue],e,this):e.yieldWith(this)}}),xd={key:\"effect/Cause/StackTrace\"},ey={key:\"effect/Cause/InterruptorStackTrace\"},Ht=AC({op:\"Failure\",prop:\"cause\",[Ke](e){let t=this[ue],r=!1;e.cache.stackFrame&&(t=yc(t,{mapUnsafe:new Map([[xd.key,e.cache.stackFrame]])}),r=!0);let n=e.getCont(Qr),o=e._interruptedCause;if(o&&e.interruptible){let s=!1;for(;n&&e.interruptible;)s||=Qg in n,n=e.getCont(Qr);s&&(t=cs(t.reasons.filter(a=>a._tag!==\"Fail\"))),t=za(t,o),r=!0}return n?n[Qr](t,e,r?void 0:this):e.yieldWith(r?Ht(t):this)}}),en=e=>Ht(gc(e)),Er=e=>Ht(Xg(e)),re=En({op:\"WithFiber\",[Ke](e){return this[ue](e)}}),kC=En({op:\"WithFiberSucceed\",[Ke](e){let t=this[ue](e),r=e.getCont(_t);return r?r[_t](t,e):e.yieldWith(Ae(t))}}),CB=(function(){class e extends globalThis.Error{}let t=Xr({op:\"YieldableError\",[Ke](){return en(this)}});return delete t.toString,Object.assign(e.prototype,t),e})(),_f=(function(){let e=Symbol.for(\"effect/Data/Error/plainArgs\");return class extends CB{constructor(r){super(r?.message,r?.cause?{cause:r.cause}:void 0),r&&(md(this,r),Object.defineProperty(this,e,{value:r,enumerable:!1}))}toJSON(){return{...this[e],...this}}}})(),us=e=>{class t extends _f{_tag=e}return t.prototype.name=e,t},kf=\"~effect/Cause/NoSuchElementError\",gd=e=>k(e,kf),Bn=class extends us(\"NoSuchElementError\"){[kf]=kf;constructor(t){super({message:t})}},Ba=\"~effect/Cause/Done\",vC=e=>k(e,Ba),OC={[Ba]:Ba,_tag:\"Done\",value:void 0},Wa=e=>e===void 0?OC:{[Ba]:Ba,_tag:\"Done\",value:e},AB=en(OC),_C=e=>e===void 0?AB:en(Wa(e));var yd=e=>Xr({op:e.label,[Ke]:e.evaluate});var xe={};Eo(xe,{Do:()=>fj,all:()=>ZB,andThen:()=>HB,as:()=>JC,asVoid:()=>zB,bind:()=>uj,bindTo:()=>ij,composeK:()=>GB,contains:()=>sj,containsWith:()=>ZC,exists:()=>aj,filter:()=>Va,filterMap:()=>ej,firstSomeOf:()=>BB,flatMap:()=>Ka,flatMapNullishOr:()=>WB,flatten:()=>Ga,fromIterable:()=>FB,fromNullOr:()=>cy,fromNullishOr:()=>Nf,fromUndefinedOr:()=>iy,gen:()=>lj,getFailure:()=>UB,getOrElse:()=>Ff,getOrNull:()=>Rd,getOrThrow:()=>uy,getOrThrowWith:()=>Pd,getOrUndefined:()=>qs,getSuccess:()=>NB,isNone:()=>fe,isOption:()=>wc,isSome:()=>be,let:()=>cj,lift2:()=>nj,liftNullishOr:()=>jB,liftPredicate:()=>oj,liftThrowable:()=>zn,makeCombinerFailFast:()=>YC,makeEquivalence:()=>tj,makeOrder:()=>rj,makeReducer:()=>pj,makeReducerFailFast:()=>dj,map:()=>Rt,match:()=>Wt,none:()=>v,orElse:()=>DB,orElseResult:()=>$B,orElseSome:()=>LB,partitionMap:()=>XB,product:()=>GC,productMany:()=>VB,reduceCompact:()=>YB,some:()=>O,tap:()=>KC,toArray:()=>QB,toRefinement:()=>MB,void:()=>qB,zipLeft:()=>KB,zipRight:()=>JB,zipWith:()=>VC});function RC(e){return{combine:e}}function ry(e,t,r){return{combine:e,initialValue:t,combineAll:r??(n=>{let o=t;for(let s of n)o=e(o,s);return o})}}var nr=e=>(t,r)=>t===r||e(t,r),kB=(e,t)=>e===t,Ja=()=>kB;function FC(e){return nr((t,r)=>{if(t.length!==r.length)return!1;for(let n=0;n{if(t.length!==r.length)return!1;for(let n=0;nl(3,(t,r,n)=>e(t,o=>({...o,[r]:n(o)}))),Ed=e=>l(2,(t,r)=>e(t,n=>({[r]:n}))),Id=(e,t)=>l(3,(r,n,o)=>t(r,s=>e(o(s),a=>({...s,[n]:a}))));var UC=\"~effect/Option\",DC={[UC]:{_A:e=>e},...rr,[Symbol.iterator](){return new Bs(this)}},vB=Object.defineProperty(Object.assign(Object.create(DC),{_tag:\"Some\",_op:\"Some\",[ae](e){return Td(e)&&ny(e)&&J(this.value,e.value)},[se](){return le(H(this._tag))(H(this.value))},toString(){return`some(${F(this.value)})`},toJSON(){return{_id:\"Option\",_tag:this._tag,value:vt(this.value)}}}),\"valueOrUndefined\",{get(){return this.value}}),OB=H(\"None\"),_B=Object.assign(Object.create(DC),{_tag:\"None\",_op:\"None\",valueOrUndefined:void 0,[ae](e){return Td(e)&&wd(e)},[se](){return OB},toString(){return\"none()\"},toJSON(){return{_id:\"Option\",_tag:this._tag}}}),Td=e=>k(e,UC),wd=e=>e._tag===\"None\",ny=e=>e._tag===\"Some\",Rf=Object.create(_B),LC=function(e){this.value=e};LC.prototype=vB;var Pf=e=>new LC(e);var BC=\"~effect/Result\",jC={[BC]:{_A:e=>e,_E:e=>e},...rr,[Symbol.iterator](){return new Bs(this)}},RB=Object.assign(Object.create(jC),{_tag:\"Success\",_op:\"Success\",[ae](e){return Cd(e)&&bc(e)&&J(this.success,e.success)},[se](){return le(H(this._tag))(H(this.success))},toString(){return`success(${F(this.success)})`},toJSON(){return{_id:\"Result\",_tag:this._tag,value:vt(this.success)}}}),PB=Object.assign(Object.create(jC),{_tag:\"Failure\",_op:\"Failure\",[ae](e){return Cd(e)&&Sc(e)&&J(this.failure,e.failure)},[se](){return le(H(this._tag))(H(this.failure))},toString(){return`failure(${F(this.failure)})`},toJSON(){return{_id:\"Result\",_tag:this._tag,failure:vt(this.failure)}}}),Cd=e=>k(e,BC),Sc=e=>e._tag===\"Failure\",bc=e=>e._tag===\"Success\",zC=function(e){this.failure=e};zC.prototype=PB;var Ad=e=>new zC(e),qC=function(e){this.success=e};qC.prototype=RB;var kd=e=>new qC(e),oy=e=>bc(e)?Rf:Pf(e.failure),sy=e=>Sc(e)?Rf:Pf(e.success);function zs(e){return(t,r)=>t===r?0:e(t,r)}var WC=zs((e,t)=>eglobalThis.Number.isNaN(e)&&globalThis.Number.isNaN(t)?0:globalThis.Number.isNaN(e)?-1:globalThis.Number.isNaN(t)?1:eezs((r,n)=>e(t(r),t(n)))),Ic=ay(fs,e=>e.getTime());var vd=e=>l(2,(t,r)=>e(t,r)===-1),Tc=e=>l(2,(t,r)=>e(t,r)===1),Od=e=>l(2,(t,r)=>e(t,r)!==1),_d=e=>l(2,(t,r)=>e(t,r)!==-1);var v=()=>Rf,O=Pf,wc=Td,fe=wd,be=ny,Wt=l(2,(e,{onNone:t,onSome:r})=>fe(e)?t():r(e.value)),MB=e=>t=>be(e(t)),FB=e=>{for(let t of e)return O(t);return v()},NB=sy,UB=oy,Ff=l(2,(e,t)=>fe(e)?t():e.value),DB=l(2,(e,t)=>fe(e)?t():e),LB=l(2,(e,t)=>fe(e)?O(t()):e),$B=l(2,(e,t)=>fe(e)?Rt(t(),kd):Rt(e,Ad)),BB=e=>{let t=v();for(t of e)if(be(t))return t;return t},Nf=e=>e==null?v():O(e),iy=e=>e===void 0?v():O(e),cy=e=>e===null?v():O(e),jB=e=>(...t)=>Nf(e(...t)),Rd=Ff($g),qs=Ff(bn),zn=e=>(...t)=>{try{return O(e(...t))}catch{return v()}},Pd=l(2,(e,t)=>{if(be(e))return e.value;throw t()}),uy=Pd(()=>new Error(\"getOrThrow called on a None\")),Rt=l(2,(e,t)=>fe(e)?v():O(t(e.value))),JC=l(2,(e,t)=>Rt(e,()=>t)),zB=JC(void 0),qB=O(void 0);var Ka=l(2,(e,t)=>fe(e)?v():t(e.value)),HB=l(2,(e,t)=>Ka(e,r=>{let n=Zr(t)?t(r):t;return wc(n)?n:O(n)})),WB=l(2,(e,t)=>fe(e)?v():Nf(t(e.value))),Ga=Ka(R),JB=l(2,(e,t)=>Ka(e,()=>t)),KB=l(2,(e,t)=>KC(e,()=>t)),GB=l(2,(e,t)=>r=>Ka(e(r),t)),KC=l(2,(e,t)=>Ka(e,r=>Rt(t(r),()=>r))),GC=l(2,(e,t)=>be(e)&&be(t)?O([e.value,t.value]):v()),VB=l(2,(e,t)=>{if(fe(e))return v();let r=[e.value];for(let n of t){if(fe(n))return v();r.push(n.value)}return O(r)}),ZB=e=>{if(Symbol.iterator in e){let r=[];for(let n of e){if(fe(n))return v();r.push(n.value)}return O(r)}let t={};for(let r of Object.keys(e)){let n=e[r];if(fe(n))return v();M(t,r,n.value)}return O(t)},VC=l(3,(e,t,r)=>Rt(GC(e,t),([n,o])=>r(n,o))),YB=l(3,(e,t,r)=>{let n=t;for(let o of e)be(o)&&(n=r(n,o.value));return n}),QB=e=>fe(e)?[]:[e.value],XB=l(2,(e,t)=>{if(fe(e))return[v(),v()];let r=t(e.value);return Sc(r)?[v(),O(r.failure)]:[O(r.success),v()]}),ej=l(2,(e,t)=>{if(fe(e))return v();let r=t(e.value);return bc(r)?O(r.success):v()}),Va=l(2,(e,t)=>fe(e)?v():t(e.value)?O(e.value):v()),tj=e=>nr((t,r)=>fe(t)?fe(r):fe(r)?!1:e(t.value,r.value)),rj=e=>zs((t,r)=>be(t)?be(r)?e(t.value,r.value):1:-1),nj=e=>l(2,(t,r)=>VC(t,r,e)),oj=l(2,(e,t)=>t(e)?O(e):v()),ZC=e=>l(2,(t,r)=>fe(t)?!1:e(t.value,r)),sj=ZC(Vg()),aj=l(2,(e,t)=>fe(e)?!1:t(e.value)),ij=Ed(Rt),cj=bd(Rt);var uj=Id(Rt,Ka),fj=O({}),lj=(...e)=>{let r=(e.length===1?e[0]:e[1].bind(e[0]))(),n=r.next();for(;!n.done;){let o=n.value;if(fe(o))return o;n=r.next(o.value)}return O(n.value)};function pj(e){return ry((t,r)=>fe(t)?r:fe(r)?t:O(e.combine(t.value,r.value)),v())}function YC(e){return RC((t,r)=>fe(t)||fe(r)?v():O(e.combine(t.value,r.value)))}function dj(e){let t=YC(e).combine,r=O(e.initialValue);return ry(t,r,n=>{let o=r;for(let s of n)if(o=t(o,s),fe(o))return o;return o})}var QC=\"~effect/Context/Service\",lr=function(){function e(){}let t=e;Object.setPrototypeOf(t,mj);let r=(n,o)=>(t.key=n,o?.defaultValue&&(t[ly]=ly,t.defaultValue=o.defaultValue),o?.make&&(t.make=o.make),o?.fiberCached&&XC.add(n),t);return arguments.length>0?r(arguments[0],arguments[1]):r},mj={[QC]:QC,...yd({label:\"Service\",evaluate(e){return Ae(Fe(e.context,this))}}),toJSON(){return{_id:\"Service\",key:this.key}},of(e){return e},context(e){return Za(this,e)},use(e){return re(t=>e(Fe(t.context,this)))},useSync(e){return re(t=>Ae(e(Fe(t.context,this))))}},XC=new Set,ly=\"~effect/Context/Reference\",eA=\"~effect/Context\",hj=8,xj=8,py=(e,t,r,n)=>{let o=Object.create(yj);return o.cacheRoot=e??o,o.base=t,o.overlay=r,o.depth=n,o._flat=void 0,o.baseHits=0,o},tA=(e,t)=>{t&&(tA(e,t.parent),e.set(t.key,t.value))},rA=e=>{if(e._flat)return e._flat;if(!e.overlay)return e._flat=e.base;let t=new Map(e.base);return tA(t,e.overlay),e._flat=t},gj=(e,t)=>{let r=new Map(e.mapUnsafe);return t(r),Oo(r)},Md=Symbol(),dy=(e,t)=>{let r=e;for(let o=r.overlay;o;o=o.parent)if(o.key===t)return o.value;let n=r.base.get(t);return n===void 0&&!r.base.has(t)?Md:(r.overlay&&++r.baseHits>=r.base.size&&r.baseHits>=xj&&(r.base=rA(r),r.overlay=void 0,r.depth=0),n)},Oo=e=>py(void 0,e,void 0,0),yj={get mapUnsafe(){return rA(this)},...rr,[eA]:{_Services:e=>e},toJSON(){return{_id:\"Context\",services:Array.from(this.mapUnsafe).map(([e,t])=>({key:e,value:t}))}},[ae](e){if(!Uf(e))return!1;let t=this.mapUnsafe,r=e.mapUnsafe;if(t.size!==r.size)return!1;for(let[n,o]of t)if(!r.has(n)||!J(o,r.get(n)))return!1;return!0},[se](){return It(this.mapUnsafe.size)}},nA=(e,t)=>e.cacheRoot===t.cacheRoot,Uf=e=>k(e,eA);var oA=e=>!!e[ly],Ir=()=>Sj,Sj=Oo(new Map),Za=(e,t)=>Oo(new Map([[e.key,t]])),Jt=l(3,(e,t,r)=>bj(e,t.key,r)),bj=(e,t,r)=>{let n=e,o=XC.has(t)?void 0:n.cacheRoot;if(n.depth>=hj){let s=new Map(n.mapUnsafe);return s.set(t,r),py(o,s,void 0,0)}return py(o,n.base,{key:t,value:r,parent:n.overlay},n.depth+1)};var Fd=l(2,(e,t)=>Df(e,t.key)),Df=(e,t)=>{let r=dy(e,t);return r===Md?void 0:r},_o=l(2,(e,t)=>{let r=dy(e,t.key);if(r===Md){if(oA(t))return sA(t);throw Ej(t)}return r}),Fe=_o,fy=\"~effect/Context/defaultValue\",sA=e=>fy in e?e[fy]:e[fy]=e.defaultValue(),Ej=e=>{let t=new Error(`Service not found${e.key?`: ${String(e.key)}`:\"\"}`);if(t.stack){let r=t.stack.split(`\n`);r.splice(1,3),t.stack=r.join(`\n`)}return t},Nd=l(2,(e,t)=>{let r=dy(e,t.key);return r!==Md?O(r):oA(t)?O(sA(t)):v()}),Ud=l(2,(e,t)=>e.mapUnsafe.size===0?t:t.mapUnsafe.size===0?e:gj(e,r=>t.mapUnsafe.forEach((n,o)=>r.set(o,n)))),aA=(...e)=>{let t=new Map;for(let r=0;r{t.set(o,n)});return Oo(t)};var we=lr;var my=e=>e.length>0;var ot=e=>e>0?Math.floor(e):0,Tn=e=>Math.max(1,ot(e));var B=kd,W=Ad;var iA=Cd,ce=Sc,mt=bc;var Dd=l(2,(e,t)=>ce(e)?W(t(e.failure)):e),Tr=l(2,(e,t)=>mt(e)?B(t(e.success)):e),wr=l(2,(e,{onFailure:t,onSuccess:r})=>ce(e)?t(e.failure):r(e.success));var Lf=l(2,(e,t)=>ce(e)?t(e.failure):e.success);var Tj=l(2,(e,t)=>{if(mt(e))return e.success;throw t(e.failure)}),$f=Tj(R);var Lt=l(2,(e,t)=>ce(e)?W(e.failure):t(e.success));var hy=e=>{if(Symbol.iterator in e){let r=[];for(let n of e){if(ce(n))return n;r.push(n.success)}return B(r)}let t={};for(let r of Object.keys(e)){let n=e[r];if(ce(n))return n;M(t,r,n.success)}return B(t)};var cA=FC;var wj=(e,t)=>{let r=t?.length!==void 0?Tn(t.length):1/0;return{[Symbol.iterator](){let n=0;return{next(){return nAj(wj(()=>e,{length:t}))),fA=e=>Cj(e,1/0);var lA=e=>{let r=e[Symbol.iterator]().next();if(r.done)throw new Error(\"headUnsafe: empty iterable\");return r.value};var pA=l(2,(e,t)=>{let r=0;for(let n of e){let o=t(n,r);if(ad(o)){if(o)return O(n)}else if(be(o))return o;r++}return v()});var Aj=e=>({[Symbol.iterator](){let t=e[Symbol.iterator](),r;function n(){for(;;){if(r===void 0){let s=t.next();if(s.done)return s;r=s.value[Symbol.iterator]()}let o=r.next();if(!o.done)return o;r=void 0}}return{next:n}}});var dA=l(2,(e,t)=>({[Symbol.iterator](){let r=e[Symbol.iterator](),n=0;return{next(){let o=r.next();for(;!o.done;){if(t(o.value,n++))return{done:!1,value:o.value};o=r.next()}return{done:!0,value:void 0}}}}}));var kj=l(2,(e,t)=>Object.hasOwn(e,t));var Ya=l(2,(e,t)=>{let r={...e};for(let n of mA(e))M(r,n,t(e[n],n));return r});var mA=e=>Object.keys(e);var vj=e=>l(2,(t,r)=>{for(let n of mA(t))if(!kj(r,n)||!e(t[n],r[n]))return!1;return!0});var hA=e=>{let t=vj(e);return(r,n)=>t(r,n)&&t(n,r)};var Ld=globalThis.Array;var xA=l(2,(e,t)=>{let r=Tn(e),n=new Ld(r);for(let o=0;oe<=t?xA(t-e+1,r=>e+r):[e];var Ye=e=>Ld.isArray(e)?e:Ld.from(e);var $d=l(2,(e,t)=>[...e,t]),gy=l(2,(e,t)=>Ye(e).concat(Ye(t)));var gA=Ld.isArray;var or=my,tt=my;function Cc(e){let t=Number(e);return String(t)===e&&Number.isInteger(t)&&t>=0&&t<2**32-1}function Oj(e,t){return!Number.isFinite(e)||e<0||e>=t.length}var yA=l(2,(e,t)=>{let r=Math.floor(t);if(Oj(r,e))throw new Error(`Index out of bounds: ${r}`);return e[r]});var Bd=e=>e[e.length-1];var yy=pA;var _j=(e,t)=>{let r=H(t),n=e.get(r);if(n===void 0)return e.set(r,[t]),!0;for(let o of n)if(J(o,t))return!1;return n.push(t),!0};var Pt=()=>[],Ee=e=>[e],wn=l(2,(e,t)=>e.map(t));var jd=l(2,(e,t)=>{let r=Ye(e),n=[];for(let o=0;o{let r=[],n=[],o=0;for(let s of e){let a=t(s,o++);mt(a)?r.push(a.success):n.push(a.failure)}return[r,n]});var SA=Sd;var zd=e=>{let t=Ye(e);if(t.length<2)return[...t];let r=new Map,n=[];for(let o of t)_j(r,o)&&n.push(o);return n};var Hd=\"~effect/Duration\",kc=BigInt(0),CA=BigInt(1),Rj=BigInt(2),Pj=BigInt(10),bA=BigInt(24),qd=BigInt(60),by=BigInt(1e3),EA=BigInt(1e6);var Wd=e=>BigInt(e<0?Math.ceil(e-.5):Math.floor(e+.5)),AA=e=>Wd(e*1e6),IA=(e,t)=>{let r=e.indexOf(\".\");if(r===-1)return BigInt(e)*t;let n=e[0]===\"-\",o=e.slice(r+1),s=Pj**BigInt(o.length),a=(BigInt(e.slice(n?1:0,r))*s+BigInt(o))*t,i=a/s+(a%s*Rj>=s?CA:kc);return n?-i:i};var Mj=/^(-?\\d+(?:\\.\\d+)?)\\s+(nanos?|micros?|millis?|seconds?|minutes?|hours?|days?|weeks?)$/,Ge=e=>{switch(typeof e){case\"number\":return Js(e);case\"bigint\":return Ro(e);case\"string\":{if(e===\"Infinity\")return ls;if(e===\"-Infinity\")return Ac;let t=Mj.exec(e);if(!t)break;let[r,n,o]=t;if(o===\"nano\"||o===\"nanos\")return Ro(IA(n,CA));if(o===\"micro\"||o===\"micros\")return Ro(IA(n,by));let s=Number(n);switch(o){case\"milli\":case\"millis\":return Js(s);case\"second\":case\"seconds\":return Iy(s);case\"minute\":case\"minutes\":return $j(s);case\"hour\":case\"hours\":return Bj(s);case\"day\":case\"days\":return jj(s);case\"week\":case\"weeks\":return zj(s)}break}case\"object\":{if(e===null)break;if(Hd in e)return e;if(Array.isArray(e))return e.length!==2||!e.every(Ef)?TA(e):Number.isNaN(e[0])||Number.isNaN(e[1])?Ws:e[0]===-1/0||e[1]===-1/0?Ac:e[0]===1/0||e[1]===1/0?ls:ar(Wd(e[0]*1e9+e[1]));let t=e,r=0;return t.weeks&&(r+=t.weeks*6048e5),t.days&&(r+=t.days*864e5),t.hours&&(r+=t.hours*36e5),t.minutes&&(r+=t.minutes*6e4),t.seconds&&(r+=t.seconds*1e3),t.milliseconds&&(r+=t.milliseconds),!t.microseconds&&!t.nanoseconds?ar(r):ar(Wd(r*1e6+(t.microseconds??0)*1e3+(t.nanoseconds??0)))}}return TA(e)},TA=e=>{throw new Error(`Invalid Input: ${e}`)},kA=zn(Ge),wA={_tag:\"Millis\",millis:0},Fj={_tag:\"Infinity\"},Nj={_tag:\"NegativeInfinity\"},Uj={[Hd]:Hd,[se](){switch(this.value._tag){case\"Millis\":{let e=this.value.millis*1e6;return Number.isFinite(e)?H(Wd(e)):It(this.value.millis)}case\"Nanos\":return H(this.value.nanos);default:return Tf(this.value)}},[ae](e){return Ey(e)&&Hj(this,e)},toString(){switch(this.value._tag){case\"Infinity\":return\"Infinity\";case\"NegativeInfinity\":return\"-Infinity\";case\"Nanos\":return`${this.value.nanos} nanos`;case\"Millis\":return`${this.value.millis} millis`}},toJSON(){switch(this.value._tag){case\"Millis\":return{_id:\"Duration\",_tag:\"Millis\",millis:this.value.millis};case\"Nanos\":return{_id:\"Duration\",_tag:\"Nanos\",nanos:String(this.value.nanos)};case\"Infinity\":return{_id:\"Duration\",_tag:\"Infinity\"};case\"NegativeInfinity\":return{_id:\"Duration\",_tag:\"NegativeInfinity\"}}},[Te](){return this.toJSON()},pipe(){return Z(this,arguments)}},ar=e=>{let t=Object.create(Uj);return typeof e==\"number\"?isNaN(e)||e===0||Object.is(e,-0)?t.value=wA:Number.isFinite(e)?Number.isInteger(e)?t.value={_tag:\"Millis\",millis:e}:t.value={_tag:\"Nanos\",nanos:AA(e)}:t.value=e>0?Fj:Nj:e===kc?t.value=wA:t.value={_tag:\"Nanos\",nanos:e},t},Ey=e=>k(e,Hd),Ks=e=>e.value._tag!==\"Infinity\"&&e.value._tag!==\"NegativeInfinity\",vc=e=>{switch(e.value._tag){case\"Millis\":return e.value.millis===0;case\"Nanos\":return e.value.nanos===kc;case\"Infinity\":case\"NegativeInfinity\":return!1}},Dj=e=>{switch(e.value._tag){case\"Millis\":return e.value.millis<0;case\"Nanos\":return e.value.nanos{switch(e.value._tag){case\"Infinity\":case\"NegativeInfinity\":return ls;case\"Millis\":return e.value.millis<0?ar(-e.value.millis):e;case\"Nanos\":return e.value.nanosar(e);var Js=e=>ar(e),Iy=e=>ar(e*1e3),$j=e=>ar(e*6e4),Bj=e=>ar(e*36e5),jj=e=>ar(e*864e5),zj=e=>ar(e*6048e5),Cr=e=>_A(Ge(e),{onMillis:R,onNanos:t=>Number(t)/1e6,onInfinity:()=>1/0,onNegativeInfinity:()=>-1/0}),vA=e=>_A(Ge(e),{onMillis:t=>t/1e3,onNanos:t=>Number(t)/1e9,onInfinity:()=>1/0,onNegativeInfinity:()=>-1/0});var Jd=e=>{let t=Ge(e);switch(t.value._tag){case\"Infinity\":case\"NegativeInfinity\":throw new Error(\"Cannot convert infinite duration to nanos\");case\"Nanos\":return t.value.nanos;case\"Millis\":return AA(t.value.millis)}},OA=zn(Jd);var _A=l(2,(e,t)=>{switch(e.value._tag){case\"Millis\":return t.onMillis(e.value.millis);case\"Nanos\":return t.onNanos(e.value.nanos);case\"Infinity\":return t.onInfinity();case\"NegativeInfinity\":return(t.onNegativeInfinity??t.onInfinity)()}}),RA=l(3,(e,t,r)=>e.value._tag===\"Infinity\"||e.value._tag===\"NegativeInfinity\"||t.value._tag===\"Infinity\"||t.value._tag===\"NegativeInfinity\"?r.onInfinity(e,t):e.value._tag===\"Millis\"?t.value._tag===\"Millis\"?r.onMillis(e.value.millis,t.value.millis):r.onNanos(Jd(e),t.value.nanos):r.onNanos(e.value.nanos,Jd(t)));var qj=(e,t)=>RA(e,t,{onMillis:(r,n)=>r===n,onNanos:(r,n)=>r===n,onInfinity:(r,n)=>r.value._tag===n.value._tag});var PA=l(2,(e,t)=>RA(e,t,{onMillis:(r,n)=>ar(r-n),onNanos:(r,n)=>ar(r-n),onInfinity:(r,n)=>{let o=r.value._tag,s=n.value._tag;return o===\"Infinity\"?s===\"Infinity\"?Ws:ls:o===\"NegativeInfinity\"?s===\"NegativeInfinity\"?Ws:Ac:s===\"Infinity\"?Ac:ls}}));var Hj=l(2,(e,t)=>qj(e,t)),Wj=e=>{if(e.value._tag===\"Infinity\")return{days:1/0,hours:1/0,minutes:1/0,seconds:1/0,millis:1/0,nanos:1/0};if(e.value._tag===\"NegativeInfinity\")return{days:-1/0,hours:-1/0,minutes:-1/0,seconds:-1/0,millis:-1/0,nanos:-1/0};let t=Jd(e),r=t{if(e.value._tag===\"Infinity\")return\"Infinity\";if(e.value._tag===\"NegativeInfinity\")return\"-Infinity\";if(vc(e))return\"0\";if(Dj(e))return\"-\"+Ty(Lj(e));let t=Wj(e),r=[];return t.days!==0&&r.push(`${t.days}d`),t.hours!==0&&r.push(`${t.hours}h`),t.minutes!==0&&r.push(`${t.minutes}m`),t.seconds!==0&&r.push(`${t.seconds}s`),t.millis!==0&&r.push(`${t.millis}ms`),t.nanos!==0&&r.push(`${t.nanos}ns`),r.join(\" \")};var wy=l(2,(e,t)=>r=>{let n=e(r);if(ce(n))return W(r);let o=t(n.success);return ce(o)?W(r):o}),MA=e=>t=>{let r=e(t);return ce(r)?v():O(r.success)};var zf=we(\"effect/Scheduler\",{fiberCached:!0,defaultValue:()=>new Qa}),NA=e=>{let t=!1;return Promise.resolve().then(()=>{t||e()}),()=>{t=!0}},Jj=\"setImmediate\"in globalThis?e=>{let t=globalThis.setImmediate(e);return()=>globalThis.clearImmediate(t)}:e=>{let t=setTimeout(e,0);return()=>clearTimeout(t)},UA=e=>{try{return Jj(e)}catch{return NA(e)}},Cy=class{buckets=[];scheduleTask(t,r){let n=this.buckets,o=n.length,s,a=0;for(;ar);a++)s=n[a];s&&s[0]===r?s[1].push(t):a===o?n.push([r,[t]]):n.splice(a,0,[r,[t]])}drain(){let t=this.buckets;return this.buckets=[],t}},Qa=class{executionMode;setImmediate;constructor(t=\"async\",r){this.executionMode=t,this.setImmediate=r??(t===\"sync\"?NA:UA)}shouldYield(t){return t.currentOpCount>=t.cache.maxOpsBeforeYield}makeDispatcher(){return new Ay(this.setImmediate)}},Ay=class{tasks=new Cy;running=void 0;setImmediate;constructor(t=UA){this.setImmediate=t}scheduleTask(t,r){this.tasks.scheduleTask(t,r),this.running===void 0&&(this.running=this.setImmediate(this.afterScheduled))}afterScheduled=()=>{this.running=void 0,this.runTasks()};runTasks(){let t=this.tasks.drain();for(let r=0;r0;)this.running!==void 0&&(this.running(),this.running=void 0),this.runTasks()}},DA=we(\"effect/Scheduler/MaxOpsBeforeYield\",{fiberCached:!0,defaultValue:()=>2048}),LA=we(\"effect/Scheduler/PreventSchedulerYield\",{fiberCached:!0,defaultValue:()=>!1});var Cn={};Eo(Cn,{Class:()=>Kd,Error:()=>Gd,TaggedClass:()=>Kj,TaggedError:()=>Fr,taggedEnum:()=>Gj});var Kd=class extends $s{constructor(e){super(),e&&md(this,e)}},Kj=e=>class extends Kd{_tag=e},Gj=()=>new Proxy({},{get(e,t,r){return t===\"$is\"?At:t===\"$match\"?Vj:n=>({...n,_tag:t})}});function Vj(){if(arguments.length===1){let r=arguments[0];return function(n){return r[n._tag](n)}}let e=arguments[0];return arguments[1][e._tag](e)}var Gd=_f,Fr=us;var $A=\"~effect/encoding/EncodingError\",An=class extends Fr(\"EncodingError\"){[$A]=$A};var zA=e=>BA(typeof e==\"string\"?Xj.encode(e):e),vy=e=>{switch(e){case 16:return Yj();case 32:return Qj();default:{let t=\"\";for(let r=e>>>3;r>0;r--)t+=Zj();return t}}},te=Uint8Array.from(\"0123456789abcdef\",e=>e.charCodeAt(0)),Xa=()=>Math.random()*4294967296>>>0,Zj=()=>{let e=Xa();return String.fromCharCode(te[e>>>28],te[e>>>24&15],te[e>>>20&15],te[e>>>16&15],te[e>>>12&15],te[e>>>8&15],te[e>>>4&15],te[e&15])},Yj=()=>{let e=Xa(),t=Xa();return String.fromCharCode(te[e>>>28],te[e>>>24&15],te[e>>>20&15],te[e>>>16&15],te[e>>>12&15],te[e>>>8&15],te[e>>>4&15],te[e&15],te[t>>>28],te[t>>>24&15],te[t>>>20&15],te[t>>>16&15],te[t>>>12&15],te[t>>>8&15],te[t>>>4&15],te[t&15])},Qj=()=>{let e=Xa(),t=Xa(),r=Xa(),n=Xa();return String.fromCharCode(te[e>>>28],te[e>>>24&15],te[e>>>20&15],te[e>>>16&15],te[e>>>12&15],te[e>>>8&15],te[e>>>4&15],te[e&15],te[t>>>28],te[t>>>24&15],te[t>>>20&15],te[t>>>16&15],te[t>>>12&15],te[t>>>8&15],te[t>>>4&15],te[t&15],te[r>>>28],te[r>>>24&15],te[r>>>20&15],te[r>>>16&15],te[r>>>12&15],te[r>>>8&15],te[r>>>4&15],te[r&15],te[n>>>28],te[n>>>24&15],te[n>>>20&15],te[n>>>16&15],te[n>>>12&15],te[n>>>8&15],te[n>>>4&15],te[n&15])},Oy=e=>{let t=new TextEncoder().encode(e);if(t.length%2!==0)return W(new An({module:\"Hex\",kind:\"Decode\",input:e,message:`Length must be a multiple of 2, but is ${t.length}`}));try{let r=t.length/2,n=new Uint8Array(r);for(let o=0;oTr(Oy(e),t=>ez.decode(t)),Xj=new TextEncoder,ez=new TextDecoder,tz=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,\"0\")),BA=e=>{let t=\"\";for(let r=0;r{if(48<=e&&e<=57)return e-48;if(97<=e&&e<=102)return e-97+10;if(65<=e&&e<=70)return e-65+10;throw new TypeError(\"Invalid input\")};var Ry=\"effect/Tracer/ParentSpan\",ei=class extends lr()(Ry,{fiberCached:!0}){},rz=e=>e;var Vd=we(\"effect/Tracer/DisablePropagation\",{defaultValue:yf}),WA=we(\"effect/Tracer/CurrentTraceLevel\",{defaultValue:()=>\"Info\"}),JA=we(\"effect/Tracer/MinimumTraceLevel\",{defaultValue:()=>\"All\"}),Py=\"effect/Tracer\",My=we(Py,{fiberCached:!0,defaultValue:()=>Fy}),Fy=rz({span:e=>new _y(e)}),_y=class{_tag=\"Span\";sampled;name;parent;annotations;links;startTime;kind;status;_traceId=void 0;_spanId=void 0;_attributes=void 0;_events=void 0;constructor(t){this.name=t.name,this.parent=t.parent,this.annotations=t.annotations,this.links=t.links,this.startTime=t.startTime,this.kind=t.kind,this.sampled=t.sampled,this.status={_tag:\"Started\",startTime:t.startTime}}get traceId(){return this._traceId??=qs(this.parent)?.traceId??vy(32)}get spanId(){return this._spanId??=vy(16)}get attributes(){return this._attributes??=new Map}get events(){return this._events??=[]}end(t,r){this.status={_tag:\"Ended\",endTime:t,exit:r,startTime:this.status.startTime}}attribute(t,r){this.attributes.set(t,r)}event(t,r,n){this.events.push([t,r,n??{}])}addLinks(t){this.links.push(...t)}};var GA=\"effect/Metric/FiberRuntimeMetrics\";var VA=we(\"effect/ErrorReporter/CurrentErrorReporters\",{defaultValue:()=>new Set}),qf=we(\"effect/References/CurrentStackFrame\",{fiberCached:!0,defaultValue:bn}),Zd=we(\"effect/References/TracerEnabled\",{fiberCached:!0,defaultValue:Sr}),ti=we(\"effect/References/TracerTimingEnabled\",{defaultValue:Sr}),Hf=we(\"effect/References/TracerSpanAnnotations\",{defaultValue:()=>({})}),Wf=we(\"effect/References/TracerSpanLinks\",{defaultValue:()=>[]}),ps=we(\"effect/References/CurrentLogAnnotations\",{defaultValue:()=>({})}),Ny=we(\"effect/References/CurrentLogLevel\",{fiberCached:!0,defaultValue:()=>\"Info\"}),Uy=we(\"effect/References/MinimumLogLevel\",{fiberCached:!0,defaultValue:()=>\"Info\"});var Yd=we(\"effect/References/CurrentLogSpans\",{defaultValue:()=>[]});var Gs=e=>{if(e?.captureStackTrace===!1)return e;if(e?.captureStackTrace!==void 0&&typeof e.captureStackTrace!=\"boolean\")return e;let t=Co();if(t===0&&e?.captureStackTrace!==!0)return{...e,captureStackTrace:!1};Yr(3);let r=new Error;return Yr(t),{...e,captureStackTrace:sz(()=>r.stack)}},Dy=e=>t=>{let r;return()=>{if(r!==void 0)return r;let n=t();if(!n)return;let o=n.split(`\n`);if(o[e]!==void 0)return r=o[e].trim(),r}},sz=Dy(3);var Qd=class extends hc{constructor(t,r=Of){super(\"Interrupt\",r,\"Interrupted\"),this.fiberId=t}toString(){return`Interrupt(${this.fiberId})`}toJSON(){return{_tag:\"Interrupt\",fiberId:this.fiberId}}[ae](t){return Ha(t)&&this.fiberId===t.fiberId&&this.annotations===t.annotations}[se](){return le(Re(`${this._tag}:${this.fiberId}`))(ud(this.annotations))}},nk=e=>new Qd(e),Zs=e=>new Ao([new Qd(e)]),Gy=e=>e.reasons.some(jn),Vy=e=>{let t=e.reasons.find(jn);return t?B(t):W(e)},Ur=e=>{for(let t=0;te.reasons.some(qa),sk=e=>{let t=e.reasons.find(qa);return t?B(t):W(e)},tl=e=>{let t=e.reasons.find(qa);return t?B(t.defect):W(e)},am=e=>e.reasons.some(Ha),ak=e=>{let t=e.reasons.find(Ha);return t?B(t):W(e)},im=e=>{let t;for(let r=0;r{let t=im(e);return ce(t)?az:t.success},az=new Set,cm=e=>e.reasons.length>0&&e.reasons.every(Ha),ck=e=>Oo(e.annotations),uk=e=>{let t=new Map;for(let r of e.reasons)if(r.annotations.size>0)for(let[n,o]of r.annotations)t.set(n,o);return Oo(t)},fk=l(2,(e,t)=>{let r=!1,n=e.reasons.map(o=>jn(o)?(r=!0,new $a(t(o.error),o.annotations)):o);return r?cs(n):e}),um=e=>{let t;for(let r of e.reasons){if(r._tag===\"Fail\")return r.error;r._tag===\"Die\"&&(t??=r)}return t!==void 0?t.defect:e.reasons.length>0?new globalThis.Error(\"All fibers interrupted without error\"):new globalThis.Error(\"Empty cause\")},Zy=(e,t)=>{let r=[],n=[];if(e.reasons.length===0)return r;let o=Co();o!==0&&Yr(1);try{for(let s of e.reasons){if(s._tag===\"Interrupt\"){n.push(s);continue}r.push($y(s._tag===\"Die\"?s.defect:s.error,s.annotations,t))}if(r.length===0){let s=new Error(\"The fiber was interrupted by:\");s.name=\"InterruptCause\",s.stack=fz(s,n);let a=new globalThis.Error(\"All fibers interrupted without error\",{cause:s});a.name=\"InterruptError\",a.stack=`${a.name}: ${a.message}`,r.push($y(a,n[0].annotations,t))}}finally{o!==0&&Yr(o)}return r},$y=(e,t,r)=>{let n=typeof e,o;if(e&&n===\"object\"){if(o=new globalThis.Error(iz(e),{cause:e.cause?$y(e.cause):void 0}),typeof e.name==\"string\"&&(o.name=e.name),typeof e.stack==\"string\")o.stack=uz(e.stack,o,t);else{let s=`${o.name}: ${o.message}`;o.stack=t?By(s,t):s}r?.includeCauseInStack&&(o.stack=pk(o));for(let s of Object.keys(e))s in o||(o[s]=e[s])}else{o=new globalThis.Error(e?n===\"string\"?e:Ua(e):`Unknown error: ${e}`);let s=`${o.name}: ${o.message}`;o.stack=t?By(s,t):s}return o},iz=e=>{if(typeof e.message==\"string\")return e.message;if(typeof e.toString==\"function\"&&e.toString!==Object.prototype.toString&&e.toString!==Array.prototype.toString)try{return e.toString()}catch{}return Ua(e)},cz=/\\((.*)\\)/g,uz=(e,t,r)=>{let n=`${t.name}: ${t.message}`,o=(e.startsWith(n)?e.slice(n.length):e).split(`\n`),s=[n];for(let a=1;a{let r=t?.get(xd.key);return r&&(e=`${e}\n${lk(r)}`),e},fz=(e,t)=>{let r=[`${e.name}: ${e.message}`];for(let n of t){let o=n.fiberId!==void 0?`#${n.fiberId}`:\"unknown\",s=n.annotations.get(ey.key);r.push(` at fiber (${o})`),s&&r.push(lk(s))}return r.join(`\n`)},lk=e=>{let t=[],r=e,n=0;for(;r&&n<10;){let o=r.stack();if(o){let s=o.matchAll(cz),a=!1;for(let[,i]of s)a=!0,t.push(` at ${r.name} (${i})`);a||t.push(` at ${r.name} (${o.replace(/^at /,\"\")})`)}else t.push(` at ${r.name}`);r=r.parent,n++}return t.join(`\n`)},fm=e=>Zy(e).map(pk).join(`\n`),pk=e=>e.cause?`${e.stack} {\n${dk(e.cause,\" \")}\n}`:e.stack,dk=(e,t)=>{let r=e.stack.split(`\n`),n=`${t}[cause]: ${r[0]}`;for(let o=1,s=r.length;o{try{return globalThis.process?.getBuiltinModule?.(\"node:async_hooks\")?.AsyncResource}catch{return}})(),mk=()=>ZA===void 0?void 0:new ZA(\"effect/Fiber\"),ds=()=>globalThis[pc],Xd=class{constructor(t,r=!0){this.setContext(t),this.id=++pz.id,this.currentOpCount=0,this.interruptible=r,this._stack=[],this._observers=void 0,this._exit=void 0,this._children=void 0,this._interruptedCause=void 0,this._yielded=void 0,this._running=!1,this._deferredInterrupt=!1,this._parent=void 0,this._asyncContext=void 0,this.cache.runtimeMetrics?.recordFiberStart(this.context)}get[Yy](){return lz}get currentDispatcher(){return this._dispatcher??=this.cache.scheduler.makeDispatcher()}getRef(t){return Fe(this.context,t)}addObserver(t){return this._exit?(t(this._exit),$n):(this._observers===void 0?this._observers=[t]:this._observers.push(t),()=>this.removeObserver(t))}removeObserver(t){if(this._exit||this._observers===void 0)return;let r=this._observers.indexOf(t);r>=0&&this._observers.splice(r,1)}interruptUnsafe(t,r){if(this._exit)return;let n=Zs(t);this.cache.stackFrame&&(n=yc(n,Za(xd,this.cache.stackFrame))),r&&(n=yc(n,r)),this._interruptedCause=this._interruptedCause?za(this._interruptedCause,n):n,this.interruptible&&(this._running?this._deferredInterrupt=!0:this.evaluate(De(this._interruptedCause)))}pollUnsafe(){return this._exit}evaluate(t){if(this._exit)return;if(this._asyncContext!==void 0){let o=this._asyncContext;return this._asyncContext=void 0,o.runInAsyncScope(this.evaluate,this,t)}else if(this._yielded!==void 0){let o=this._yielded;this._yielded=void 0,o()}let r=this.runLoop(t);if(r===ja){this._asyncContext=mk();return}let n=jy.interruptChildren&&jy.interruptChildren(this);if(n!==void 0)return this.evaluate(V(n,()=>r));if(this._exit=r,this.cache.runtimeMetrics?.recordFiberEnd(this.context,this._exit),this._parent&&(this._parent._children?.delete(this),this._parent=void 0),this._observers!==void 0){let o=this._observers;this._observers=void 0;for(let s=0;si)}if(s=a.tracerContext?a.tracerContext(s,this):s[Ke](this),s===ja){let i=this._yielded;if(Da in i)return this._deferredInterrupt=!1,this._yielded=void 0,i;if(this._deferredInterrupt){this._yielded=void 0,i();continue}return ja}}catch(a){if(!k(s,Ke))return Er(`Fiber.runLoop: Not a valid effect: ${String(s)}`);s=Er(a)}}finally{this._running=n,globalThis[pc]=r}}getCont(t){if(this._deferredInterrupt)return this._deferredInterrupt=!1,mz;for(;;){let r=this._stack.pop();if(!r)return;let n=r[La];if(n!==void 0){let o=n.call(r,this);if(o)return o[t]=o,o}if(r[t])return r}}succeedWith(t){if((++this.currentOpCount&hz-1)===0)return Ae(t);let r=this.getCont(_t);return r?r[_t](t,this):this.yieldWith(Ae(t))}yieldWith(t){return this._yielded=t,ja}children(){return this._children??=new Set}pipe(){return Z(this,arguments)}setContext(t){let r=this.context;if(this.context=t,r!==void 0&&nA(r,t))return;let n=t.cacheRoot,o=n._fiberCache??=dz(t);this.cache?.scheduler!==o.scheduler&&(this._dispatcher=void 0),this.cache=o}get currentSpanLocal(){let t=this.cache.span;return t?._tag===\"Span\"?t:void 0}},dz=e=>{let t=Df(e,Py);return{scheduler:Fe(e,zf),tracer:t,tracerContext:t?t.context:void 0,tracerEnabled:Fe(e,Zd),span:Df(e,Ry),logLevel:Fe(e,Ny),minimumLogLevel:Fe(e,Uy),stackFrame:Fe(e,qf),runtimeMetrics:Df(e,GA),maxOpsBeforeYield:Fe(e,DA),preventYield:Fe(e,LA)}},mz={[_t](e,t){return De(t._interruptedCause)},[Qr](e,t){return De(t._interruptedCause)}},hz=32,jy={interruptChildren:void 0},lm=e=>{if(!e.cache.stackFrame)return;let t=new Map;return t.set(ey.key,e.cache.stackFrame),Oo(t)},xz=e=>{if(!(e._children===void 0||e._children.size===0))return Oc(e._children)},Qy=e=>{let t=e;return t._exit?D(t._exit):ht(r=>t._exit?r(D(t._exit)):z(e.addObserver(n=>r(D(n)))))},pm=e=>ht(t=>{let r=e[Symbol.iterator](),n=[],o;function s(){let a=r.next();for(;!a.done;){if(a.value._exit){n.push(a.value._exit),a=r.next();continue}o=a.value.addObserver(i=>{n.push(i),s()});return}t(D(n))}return s(),z(()=>o?.())}),Xy=e=>{let t=e;return t._exit?t._exit:ht(r=>t._exit?r(t._exit):z(e.addObserver(r)))},hk=e=>ht(t=>{let r=Array.from(e);if(r.length===0)return t(D(Pt()));let n=new Array(r.length),o=Pt(),s=()=>{for(let c=0;c{if(a++,f._tag===\"Failure\")return i=!0,s(),t(f);n[c]=f.value,a===r.length&&t(D(n))};o.push(u),r[c].addObserver(u)}return z(()=>{i=!0,s()})}),rl=e=>re(t=>xk(e,t.id)),xk=l(e=>k(e[0],Yy),(e,t,r)=>re(n=>{let o=lm(n);return o=o&&r?Ud(o,r):o??r,e.interruptUnsafe(t,o),nl(Qy(e))})),Oc=e=>re(t=>{let r=lm(t),n=Pt();for(let o of e)o.interruptUnsafe(t.id,r),n.push(o);return nl(pm(n))});var D=Ae,De=Ht,Qe=en,z=En({op:\"Sync\",[Ke](e){let t=this[ue](),r=e.getCont(_t);return r?r[_t](t,e):e.yieldWith(Ae(t))}}),ne=En({op:\"Suspend\",[Ke](e){return this[ue]()}}),eS=l(e=>e.length>=2||wc(e[0]),(e,t)=>fe(e)?Qe(t?t():new Bn(\"Effect.fromOption: Option.none\")):D(e.value)),Pc=wr({onFailure:Qe,onSuccess:D}),gk=e=>e==null?Qe(new Bn):D(e),tS=En({op:\"Yield\",[Ke](e){let t=!1;return e.currentDispatcher.scheduleTask(()=>{t||e.evaluate(pr)},this[ue]??0),e.yieldWith(()=>{t=!0})}}),Mc=tS(0),rS=e=>D(O(e)),oi=D(v()),yk=e=>fe(e)?oi:ke(e.value,O),Sk=e=>ne(()=>De(e())),Ys=e=>Er(e),dm=e=>ne(()=>Qe(e())),pe=D(void 0);var bk=e=>{let t=typeof e==\"function\"?e:e.try,r=typeof e==\"function\"?n=>new Rc(n,\"An error occurred in Effect.try\"):e.catch;return ne(()=>{try{return D(t())}catch(n){return Qe(r(n))}})};var nS=e=>oS(function(t,r){e(r).then(n=>t(D(n)),n=>t(Ys(n)))},e.length!==0),Ek=e=>{let t=typeof e==\"function\"?e:e.try,r=typeof e==\"function\"?n=>new Rc(n,\"An error occurred in Effect.tryPromise\"):e.catch;return oS(function(n,o){let s=a=>ne(()=>{try{return Qe(js(()=>r(a)))}catch(i){return Ys(i)}});try{t(o).then(a=>n(D(a)),a=>n(s(a)))}catch(a){n(s(a))}},t.length!==0)},Ik=e=>re(t=>e(t.id)),Tk=re(D),wk=Ik(D),oS=(function(){let e=Xr({op:\"Async\",[Ke](r){let n=!1,o=!1,s=this.withSignal?new AbortController:void 0,a=this.register.call(r.cache.scheduler,i=>{n||(n=!0,o?r.evaluate(i):o=i)},s?.signal);return o!==!1?o:(o=!0,r._yielded=()=>{n=!0},s===void 0&&a===void 0?ja:(r._stack.push(gz(()=>(n=!0,s?.abort(),a??pr))),ja))}}),t=function(r,n){this.register=r,this.withSignal=n};return t.prototype=e,function(r,n){return new t(r,n)}})(),gz=En({op:\"AsyncFinalizer\",[La](e){e.interruptible&&(e.interruptible=!1,e._stack.push(fl))},[Qr](e,t){return am(e)?V(Ov(Ht(e),this[ue]()),()=>De(e)):De(e)}}),ht=e=>oS(e,e.length>=2),mm=ht($n),Ck=(...e)=>{if(e.length===1){let n=e[0];return ne(()=>ni(n()))}let[t,r]=e;return ne(()=>ni(r.call(t.self)))},sS=(e,...t)=>{let r=t.length===0?function(){return ne(()=>ni(e.apply(this,arguments)))}:function(){let n=ne(()=>ni(e.apply(this,arguments)));for(let o=0;oObject.defineProperty(t,\"length\",{value:e,configurable:!0}),YA=Dy(2),Ak=function(){let e=typeof arguments[0]==\"string\",t=e?arguments[0]:\"Effect.fn\",r=e?arguments[1]:void 0,n=Co(),o;return n!==0&&(Yr(2),o=new globalThis.Error,Yr(n)),e?(s,...a)=>QA(t,s,o,a,e,r):QA(t,arguments[0],o,Array.prototype.slice.call(arguments,1),e,r)},QA=(e,t,r,n,o,s)=>{let a=typeof t==\"function\"?t:n.shift().bind(t.self),i=`${e} (definition)`,c=r?YA(()=>r.stack):bn;return aS(a.length,function(...u){let f=ne(()=>{let m=a.apply(this,arguments);return ie(m)?m:ni(m)});for(let m=0;mJy(f,m)):f,qf,m=>({name:e,stack:d?YA(()=>d.stack):bn,parent:{name:i,stack:c,parent:m}}))})},kk=(e,...t)=>aS(e.length,t.length===0?function(){return XA(()=>e.apply(this,arguments))}:function(){let r=XA(()=>e.apply(this,arguments));for(let n of t)r=n(r,...arguments);return r}),XA=e=>{try{let t=e(),r;for(;;){let n=t.next(r);if(n.done)return D(n.value);let o=n.value;if(o&&o._tag===\"Success\"){r=o.value;continue}else{if(o&&o._tag===\"Failure\")return n.value;{let s=!0;return ne(()=>s?(s=!1,V(n.value,a=>ni(t,a))):ne(()=>ni(e())))}}}}catch(t){return Ys(t)}},ni=(function(){let e=Xr({op:\"Iterator\",[_t](r,n){let o=this.iterator;for(;;){let s=o.next(r);if(s.done)return D(s.value);if(Ne(s.value)){if(s.value._tag===\"Failure\")return s.value}else return n._stack.push(this),s.value;r=s.value.value}},[Ke](r){return this[_t](this.initial,r)}}),t=function(r,n){this.iterator=r,this.initial=n};return t.prototype=e,function(r,n){return new t(r,n)}})(),Ar=l(2,(e,t)=>new ms(e,iS,D(t))),hm=function(e){return e._stack.push(this),this[ue]},yz=Xr({op:\"OnSuccess\",[Ke]:hm}),ms=function(e,t,r){this[ue]=e,this[_t]=t,this.payload=r};ms.prototype=yz;var iS=function(){return this.payload},vk=function(e,t){return t.succeedWith(this.payload)},xm=(()=>{let e=\"~effect/Effect/stackProbe\";return{[e]:function(){return new Error().stack}}[e]()?.includes(\"[as \"+e+\"]\")===!0})(),Sz=function(e,t){let r=this.payload;return t.succeedWith(xm?r(e):js(()=>r(e)))},Ok=function(e){let t=this.payload;return t(e)},bz=function(e){let t=this.payload;return new ms(t(e),vk,e)},Ez=function(e){return new ms(this.payload,vk,e)},Fc=e=>ke(e,O),_k=e=>Dc(e,{onFailure:D,onSuccess:Qe}),Fo=l(2,(e,t)=>new ms(e,ie(t)?iS:Ok,t)),si=l(2,(e,t)=>new ms(e,ie(t)?Ez:bz,t)),nl=e=>new ms(e,iS,pr),Rk=e=>nn(e,Qe),cS=(e,t)=>re(r=>{let n=new Set;return $c(r,()=>n.size===0?void 0:Oc(n)),ht(o=>{let s=Ye(e),a=s.length,i=0,c=!1,u=[],f=(p,d,m)=>{if(i++,p._tag===\"Failure\"){u.push(...p.cause.reasons),i>=a&&o(De(cs(u)));return}let x=!c;c=!0,o(p),x&&t?.onWinner&&t.onWinner({fiber:d,index:m,parentFiber:r})};for(let p=0;p{n.delete(d),f(m,d,p)}),c)break}})}),uS=(e,t)=>re(r=>{let n=new Set;return $c(r,()=>n.size===0?void 0:Oc(n)),ht(o=>{let s=!1,a=0;for(let i of e){if(s)break;let c=a++,u=ra(r,i,!0,!0,!1);n.add(u),u.addObserver(f=>{n.delete(u);let p=!s;s=!0,o(f),p&&t?.onWinner&&t.onWinner({fiber:u,index:c,parentFiber:r})})}})}),Pk=l(e=>ie(e[1]),(e,t,r)=>cS([e,t],r)),fS=l(e=>ie(e[1]),(e,t,r)=>uS([e,t],r)),V=l(2,(e,t)=>new ms(e,Ok,t)),Mk=l(2,(e,t)=>Ne(e)?e._tag===\"Success\"?t.onSuccess(e.value):t.onFailure(e.cause):Mo(e,t)),Ne=e=>e[Da]!==void 0,Fk=l(2,(e,t)=>Ne(e)?e._tag===\"Success\"?t(e.value):e:V(e,t)),Nk=e=>V(e,R),ke=l(2,(e,t)=>new ms(e,Sz,t)),Uk=l(2,(e,t)=>Ne(e)?mS(e,t):ke(e,t)),Dk=l(2,(e,t)=>Ne(e)?hS(e,t):wS(e,t)),Lk=l(2,(e,t)=>Ne(e)?xS(e,t):CS(e,t)),$k=l(2,(e,t)=>{if(Ne(e)){if(e._tag===\"Success\")return e;let r=Ur(e.cause);return ce(r)?e:t(r.success)}return gs(e,t)}),Nc=e=>Ht(Zs(e)),Qs=e=>e._tag===\"Success\",Bk=e=>e._tag===\"Success\"?B(e):W(e),jk=e=>e._tag===\"Success\"?B(e.value):W(e),lS=e=>e._tag===\"Failure\",zk=e=>e._tag===\"Failure\"?B(e):W(e),ol=e=>e._tag===\"Failure\"?B(e.cause):W(e),pS=wy(ol,Ur),qk=wy(ol,tl),dS=e=>e._tag===\"Failure\"&&am(e.cause),Hk=e=>e._tag===\"Failure\"&&sm(e.cause),Wk=e=>e._tag===\"Failure\"&&Gy(e.cause),pr=Ae(void 0),mS=l(2,(e,t)=>e._tag===\"Success\"?Ae(t(e.value)):e),hS=l(2,(e,t)=>{if(e._tag===\"Success\")return e;let r=Ur(e.cause);return ce(r)?e:en(t(r.success))}),xS=l(2,(e,t)=>{if(e._tag===\"Success\")return Ae(t.onSuccess(e.value));let r=Ur(e.cause);return ce(r)?e:en(t.onFailure(r.success))}),Iz=l(2,(e,t)=>Qs(e)?Ae(t):e),Jk=l(2,(e,t)=>Qs(e)?t:e),Kk=l(2,(e,t)=>Qs(e)?t.onSuccess(e.value):t.onFailure(e.cause)),Gk=Iz(void 0),gS=e=>{let t=[];for(let r of e)r._tag===\"Failure\"&&t.push(...r.cause.reasons);return t.length===0?pr:Ht(cs(t))},Vk=e=>Qs(e)?O(e.value):v(),Zk=e=>lS(e)?O(e.cause):v(),Yk=e=>{let t=pS(e);return ce(t)?v():O(t.success)},Qk=e=>e,Xk=e=>re(t=>D(Nd(t.context,e))),Tz=e=>re(t=>eS(Nd(t.context,e))),Uc=l(2,(e,t)=>re(r=>{let n=r.context,o=t(n);return n===o||(r.setContext(o),$c(r,()=>{r.setContext(n)})),e})),hs=l(3,(e,t,r)=>Uc(e,n=>{let o=_o(n,t),s=r(o);return o===s?n:Jt(n,t,s)})),ev=(e,t,r)=>ii(re(n=>{let o=_o(n.context,e),s=t(o);return n.setContext(Jt(n.context,e,s)),ta(_o(n.context,ea),a=>{let i=Nd(n.context,e);if(fe(i))return pe;let c=i.value,u;if(r?.reset===void 0){if(c!==s)return pe;u=o}else u=r.reset(o,s,c);return n.setContext(Jt(n.context,e,u)),pe})})),yS=()=>wz,wz=re(e=>D(e.context)),Xs=e=>re(t=>e(t.context)),tv=l(2,(e,t)=>Uc(e,je(t))),rn=l(2,(e,t)=>Ne(e)?e:Uc(e,Ud(t))),kr=function(){return arguments.length===1?l(2,(e,t)=>ek(e,arguments[0],t)):l(3,(e,t,r)=>ek(e,t,r)).apply(this,arguments)},ek=(e,t,r)=>Uc(e,Jt(t,r)),gm=l(3,(e,t,r)=>V(r,n=>kr(e,t,n))),rv=l(e=>ie(e[1]),(e,t,r)=>SS(e,t,(n,o)=>[n,o],r)),SS=l(e=>ie(e[1]),(e,t,r,n)=>n?.concurrent?ke(ll([e,t],{concurrency:2}),([o,s])=>r(o,s)):V(e,o=>ke(t,s=>r(o,s)))),nv=l(e=>ie(e[0]),(e,t,r)=>BS(e,t,r?n=>Qe(r(n)):()=>Qe(new Bn))),ov=l(2,(e,t)=>V(t,r=>r?Fc(e):oi)),bS=l(2,(e,t)=>Array.from({length:t},()=>e)),sv=l(e=>ie(e[0]),(e,t,r)=>ll(bS(e,t),r)),ym=l(e=>ie(e[0]),(e,t)=>No({while:Sr,body:je(t?.disableYield?e:V(e,r=>Mc)),step:$n})),nn=l(2,(e,t)=>new av(e,t.length!==1?r=>t(r):t)),Cz=Xr({op:\"OnFailure\",[Ke]:hm}),av=function(e,t){this[ue]=e,this[Qr]=t};av.prototype=Cz;var ES=l(3,(e,t,r)=>nn(e,n=>t(n)?r(n):De(n))),xs=l(3,(e,t,r)=>nn(e,n=>{let o=t(n);return ce(o)?De(o.failure):r(o.success,n)})),gs=l(2,(e,t)=>xs(e,Ur,r=>t(r))),iv=e=>Dc(e,{onFailure:t=>gd(t)?oi:Qe(t),onSuccess:rS}),cv=l(2,(e,t)=>xs(e,tl,t)),uv=l(2,(e,t)=>nn(e,r=>Fo(t(r),De(r)))),fv=l(3,(e,t,r)=>ES(e,t,n=>Fo(r(n),De(n)))),Sm=l(3,(e,t,r)=>nn(e,n=>{let o=t(n);return ce(o)?De(n):Fo(r(o.success,n),De(n))})),IS=l(2,(e,t)=>Sm(e,Ur,r=>t(r))),lv=l(3,(e,t,r)=>{let n=Array.isArray(t)?o=>k(o,\"_tag\")&&t.includes(o._tag):At(t);return IS(e,o=>n(o)?r(o):pe)}),pv=l(2,(e,t)=>Sm(e,tl,r=>t(r))),sl=l(e=>ie(e[0]),(e,t,r,n)=>nn(e,o=>{let s=Ur(o);return ce(s)?De(s.failure):t(s.success)?r(s.success):n?n(s.success):De(o)})),bm=l(e=>ie(e[0]),(e,t,r,n)=>nn(e,o=>{let s=Ur(o);if(ce(s))return De(s.failure);let a=t(s.success);return ce(a)?n?n(a.failure):De(o):r(a.success)})),TS=l(e=>ie(e[0]),(e,t,r,n)=>{let o=Array.isArray(t)?s=>k(s,\"_tag\")&&t.includes(s._tag):At(t);return sl(e,o,r,n)}),dv=l(e=>ie(e[0]),(e,t,r)=>{let n;return bm(e,o=>(n??=Object.keys(t),k(o,\"_tag\")&&br(o._tag)&&n.includes(o._tag)?B(o):W(o)),o=>t[o._tag](o),r)}),mv=l(e=>ie(e[0]),(e,t,r,n,o)=>sl(e,s=>At(s,t)&&k(s,\"reason\")&&(o!==void 0||At(s.reason,r)),s=>{let a=s.reason;return At(a,r)?n(a,s):o?o(a,s):Qe(s)})),hv=l(e=>ie(e[0]),(e,t,r,n)=>{let o;return sl(e,s=>At(s,t)&&k(s,\"reason\")&&k(s.reason,\"_tag\")&&br(s.reason._tag)&&(n!==void 0||(o??=Object.keys(r)).includes(s.reason._tag)),s=>{let a=s.reason;return o??=Object.keys(r),o.includes(a._tag)?r[a._tag](a,s):n?n(a,s):Qe(s)})}),xv=l(2,(e,t)=>bm(e,r=>At(r,t)&&k(r,\"reason\")?B(r.reason):W(r),Qe)),wS=l(2,(e,t)=>gs(e,r=>dm(()=>t(r)))),CS=l(2,(e,t)=>Dc(e,{onFailure:r=>dm(()=>t.onFailure(r)),onSuccess:r=>z(()=>t.onSuccess(r))})),AS=e=>gs(e,Ys),gv=l(2,(e,t)=>gs(e,r=>z(()=>t(r)))),yv=e=>ne(()=>{let t=e[Symbol.iterator](),r=t.next();if(r.done)return Ys(new Error(\"Received an empty collection of effects\"));function n(o){let s=t.next();return s.done?o.value:gs(o.value,a=>n(s))}return n(r)}),kS=e=>gs(e,t=>V(Mc,()=>kS(e))),Sv=l(e=>ie(e[0]),(e,t)=>{if(!t?.log)return Dc(e,{onFailure:n=>pe,onSuccess:n=>pe});let r=Jn(t.log===!0?void 0:t.log);return Mo(e,{onFailure(n){let o=Vy(n);return ce(o)?De(o.failure):t.message===void 0?r(n):r(t.message,n)},onSuccess:n=>pe})}),Em=l(e=>ie(e[0]),(e,t)=>{if(!t?.log)return Mo(e,{onFailure:n=>pe,onSuccess:n=>pe});let r=Jn(t.log===!0?void 0:t.log);return Mo(e,{onFailure:n=>t.message===void 0?r(n):r(t.message,n),onSuccess:n=>pe})}),bv=e=>Im(e,{onFailure:v,onSuccess:O}),Vs=e=>al(e,{onFailure:W,onSuccess:B}),Mo=l(2,(e,t)=>new Ev(e,t.onSuccess.length!==1?r=>t.onSuccess(r):t.onSuccess,t.onFailure.length!==1?r=>t.onFailure(r):t.onFailure)),Az=Xr({op:\"OnSuccessAndFailure\",[Ke]:hm}),Ev=function(e,t,r){this[ue]=e,this[_t]=t,this[Qr]=r};Ev.prototype=Az;var kz=function(e,t){let r=this.payload;return t.succeedWith(xm?r.onSuccess(e):js(()=>r.onSuccess(e)))},Iv=(e,t)=>{let r=Xr({op:e,[Ke]:hm,[_t]:kz,[Qr]:t}),n=function(o,s){this[ue]=o,this.payload=s};return n.prototype=r,n},vz=Iv(\"Match\",function(e,t){let r=e.reasons.find(jn);if(r===void 0)return De(e);let n=this.payload;return t.succeedWith(xm?n.onFailure(r.error):js(()=>n.onFailure(r.error)))}),Oz=Iv(\"MatchCause\",function(e,t){let r=this.payload;return t.succeedWith(xm?r.onFailure(e):js(()=>r.onFailure(e)))}),vS=l(2,(e,t)=>new Oz(e,t)),Dc=l(2,(e,t)=>Mo(e,{onFailure:r=>{let n=r.reasons.find(jn);return n?t.onFailure(n.error):De(r)},onSuccess:t.onSuccess})),Im=l(2,(e,t)=>new vz(e,t)),al=l(2,(e,t)=>{if(Ne(e)){if(e._tag===\"Success\")return Ae(t.onSuccess(e.value));let r=Ur(e.cause);return ce(r)?e:Ae(t.onFailure(r.success))}return Im(e,t)}),Tv=l(2,(e,t)=>Ne(e)?e._tag===\"Success\"?Ae(t.onSuccess(e.value)):Ae(t.onFailure(e.cause)):vS(e,t)),il=e=>Ne(e)?Ae(e):_z(e),_z=En({op:\"Exit\",[Ke](e){return e._stack.push(this),this[ue]},[_t](e,t,r){return t.succeedWith(r??Ae(e))},[Qr](e,t,r){return t.succeedWith(r??Ht(e))}}),wv=al({onFailure:()=>!0,onSuccess:()=>!1}),Cv=al({onFailure:()=>!1,onSuccess:()=>!0}),Av=l(2,(e,t)=>Fo(Bc(t),e)),OS=l(2,(e,t)=>V(_S(e,t.duration),r=>fe(r)?t.orElse():D(r.value))),Rz=e=>new Vf(`Operation timed out after '${Ty(e)}'`),kv=l(2,(e,t)=>{let r=Ge(t);return OS(e,{duration:r,orElse:()=>Qe(Rz(r))})}),_S=l(2,(e,t)=>fS(Fc(e),Ar(Bc(t),v()))),vv=e=>Wn(t=>{let r=t.monotonicTimeNanosUnsafe();return ke(e,n=>[Ro(t.monotonicTimeNanosUnsafe()-r),n])}),zy=\"~effect/Scope\",qy=\"~effect/Scope/Closeable\",ea=lr(\"effect/Scope\"),RS=(e,t)=>re(r=>(Nz(r),Tm(e,t)??pe)),Tm=(e,t)=>{let r=e.state;if(r._tag===\"Closed\"||(e.state={_tag:\"Closed\",exit:t},e.parent!==void 0&&tm(e.parent,e),r._tag===\"Empty\"))return;if(r.finalizer!==void 0)return em(r.finalizer,t);let n=r.finalizers;return n.size===1?em(n.values().next().value,t):Pz(e,n,t)},em=(e,t)=>{try{return e(t)}catch(r){return Er(r)}},Ov=(e,t)=>Qs(e)?t:nn(t,r=>De(za(e.cause,r))),Pz=sS(function*(e,t,r){let n=[],o=[],s=Array.from(t.values()),a=ds();for(let i=s.length-1;i>=0;i--){let c=s[i];e.strategy===\"sequential\"?n.push(yield*il(em(c,r))):o.push(ra(a,em(c,r),!0,!0,\"inherit\"))}return o.length>0&&(n=yield*pm(o)),yield*gS(n)});var _v=(e,t)=>{let r=Pv(t,e);return e.state._tag===\"Closed\"?(r.state=e.state,r):(Lc(e,r,n=>RS(r,n)),r)},ta=(e,t)=>ne(()=>e.state._tag===\"Closed\"?t(e.state.exit):(Lc(e,{},t),pe)),Rv=(e,t)=>ta(e,je(t)),Lc=(e,t,r)=>{if(e.state._tag===\"Empty\")e.state={_tag:\"Open\",finalizerKey:t,finalizer:r,finalizers:void 0};else if(e.state._tag===\"Open\"){let n=e.state;n.finalizer!==void 0&&(n.finalizers=new Map([[n.finalizerKey,n.finalizer]]),n.finalizerKey=void 0,n.finalizer=void 0),n.finalizers.set(t,r)}},tm=(e,t)=>{if(e.state._tag!==\"Open\")return;let r=e.state;r.finalizerKey===t?e.state=Hy:r.finalizers!==void 0&&(r.finalizers.delete(t),r.finalizers.size===0&&(e.state=Hy))},wm=e=>Pv(e,void 0),Pv=(e=\"sequential\",t)=>({[qy]:qy,[zy]:zy,strategy:e,parent:t,state:Hy}),Hy={_tag:\"Empty\"};var cl=ea,Mv=kr(ea),PS=e=>re(t=>{let r=t.context,n=wm();return t.setContext(Jt(t.context,ea,n)),$c(t,o=>(t.setContext(r),Tm(n,o))),e});var Cm=e=>ne(()=>{let t=wm();return on(e(t),r=>ne(()=>Tm(t,r)??pe))}),ai=(e,t,r)=>Xs(n=>Hn(o=>V(cl,s=>si(r?.interruptible?o(e):e,a=>ta(s,i=>rn(t(a,i),n)))))),MS=e=>V(cl,t=>Xs(r=>ta(t,n=>rn(e(n),r)))),Fv=(function(){let e=Xr({op:\"OnExit\",[Ke](r){return r._stack.push(this),this.effect},[La](r){r.interruptible&&this.interruptible!==!0&&(r._stack.push(fl),r.interruptible=!1)},[_t](r,n,o){o??=Ae(r);let s;try{s=this.onExit(o)}catch(a){s=Er(a)}return s?V(s,a=>o):o},[Qr](r,n,o){o??=Ht(r);let s;try{s=this.onExit(o)}catch(a){s=Er(a)}return s?V(Ov(o,s),a=>o):o}}),t=function(r,n,o){this.effect=r,this.onExit=n,this.interruptible=o};return t.prototype=e,t})(),qn=(e,t,r)=>new Fv(e,t,r),$c=(e,t)=>{e._stack.push(new Fv(void 0,t,void 0))},on=l(2,qn),Nv=l(2,(e,t)=>on(e,r=>t)),FS=l(3,(e,t,r)=>on(e,n=>t(n)?r(n):pe)),NS=l(3,(e,t,r)=>on(e,n=>{let o=t(n);return ce(o)?pe:r(o.success,n)})),US=l(2,(e,t)=>NS(e,ol,t)),Uv=l(3,(e,t,r)=>FS(e,n=>n._tag!==\"Failure\"?!1:t(n.cause),n=>r(n.cause))),DS=l(3,(e,t,r)=>on(e,n=>{if(n._tag!==\"Failure\")return pe;let o=t(n.cause);return ce(o)?pe:r(o.success,n.cause)})),Dv=l(2,(e,t)=>DS(im,t)(e)),ul=(e,t,r)=>Hn(n=>V(e,o=>qn(ne(()=>n(t(o))),s=>r(o,s),!0))),Lv=e=>ai(e,t=>k(t,Symbol.asyncDispose)?nS(()=>t[Symbol.asyncDispose]()):z(()=>t[Symbol.dispose]())),LS=l(2,(e,t)=>z(()=>$v(e,typeof t==\"function\"?r=>Cr(Ge(t(r))):je(Cr(Ge(t)))))),Mz=je(1/0),$v=(e,t)=>{let r=0,n,o,s=(a,i)=>(i.awaiters++,$c(a,()=>{if(!(--i.awaiters>0||o!==i))return o=void 0,rl(i.fiber)}),Xy(i.fiber));return[re(a=>{if(o!==void 0)return s(a,o);if(n!==void 0&&(r===1/0||a.getRef(Nr).currentTimeMillisUnsafe(){if(o!==c||(o=void 0,dS(u)))return;let f=t(u);r=f===1/0?1/0:i.currentTimeMillisUnsafe()+f,n=u}),!0,!0),c.fiber._exit??s(a,c)}),z(()=>{n=void 0})]},Bv=l(2,(e,t)=>ke(LS(e,t),r=>r[0])),jv=e=>z(()=>$v(e,Mz)[0]),zv=re(e=>De(Zs(e.id))),ii=e=>re(t=>(t.interruptible&&(t.interruptible=!1,t._stack.push(fl)),e)),qv=En({op:\"SetInterruptible\",[La](e){if(e.interruptible=this[ue],e._interruptedCause&&e.interruptible)return()=>De(e._interruptedCause)}}),fl=qv(!0),Fz=qv(!1),Hv=e=>{if(e.interruptible=!0,e._stack.push(Fz),e._interruptedCause)return De(e._interruptedCause)},Nz=e=>{let t=e;t.interruptible&&(t.interruptible=!1,t._stack.push(fl))};var Am=e=>re(t=>t.interruptible?e:Hv(t)??e),Hn=e=>re(t=>t.interruptible?(t.interruptible=!1,t._stack.push(fl),e(Am)):e(R)),Wv=e=>re(t=>{if(t.interruptible)return e(R);let r=Hv(t),n=e(ii);return r??n}),Jv=ke(ai(z(()=>new AbortController),e=>z(()=>e.abort())),e=>e.signal),ll=(e,t)=>as(e)?t?.mode===\"result\"?tn(e,Vs,t):tn(e,R,t):t?.discard?t.mode===\"result\"?tn(Object.values(e),Vs,t):tn(Object.values(e),R,t):ne(()=>{let r={};return Ar(tn(Object.entries(e),([n,o])=>ke(t?.mode===\"result\"?Vs(o):o,s=>{M(r,n,s)}),{discard:!0,concurrency:t?.concurrency}),r)}),$S=l(e=>as(e[0])&&!ie(e[0]),(e,t,r)=>ke(tn(e,(n,o)=>Vs(t(n,o)),r),n=>Sy(n,R))),Kv=l(3,(e,t,r)=>{let n=Ye(e);return n.length===0?z(t):ne(()=>{let o=0,s=t();return ke(No({while:()=>or(s,n[o],o),step(a){s=a,o++}}),()=>s)})}),Gv=l(e=>as(e[0])&&!ie(e[0]),(e,t,r)=>V($S(e,t,{concurrency:r?.concurrency}),([n,o])=>or(o)?Qe(o):r?.discard?pe:D(n))),Vv=l(e=>as(e[0])&&!ie(e[0]),(e,t)=>ne(()=>{let r=e[Symbol.iterator](),n=r.next();return n.done?D(v()):Zv(r,0,t,n.value)})),Zv=(e,t,r,n)=>V(r(n,t),o=>{if(o)return D(O(n));let s=e.next();return s.done?D(v()):Zv(e,t+1,r,s.value)}),Yv=l(e=>as(e[0])&&!ie(e[0]),(e,t)=>ne(()=>{let r=e[Symbol.iterator](),n=r.next();return n.done?D(v()):Qv(r,0,t,n.value)})),Qv=(e,t,r,n)=>V(r(n,t),o=>{if(mt(o))return D(O(o.success));let s=e.next();return s.done?D(v()):Qv(e,t+1,r,s.value)}),No=En({op:\"While\",[_t](e,t){return this[ue].step(e),this[ue].while()?(t._stack.push(this),this[ue].body()):pr},[Ke](e){return this[ue].while()?(e._stack.push(this),this[ue].body()):pr}}),tn=l(e=>typeof e[1]==\"function\",(e,t,r)=>ne(()=>{let n=pl(r?.concurrency);if(n===1)return Uz(e,t,r);let o=Ye(e),s=o.length;if(s===0)return r?.discard?pe:D([]);let a=r?.discard?void 0:new Array(s),i=Dz({f:t,out:a},o,{concurrency:n});return i?Ar(i,a):D(a)})),Xv=e=>V(e,t=>{let r=t[Symbol.iterator]().next();return r.done?Qe(new Bn):D(r.value)}),Uz=(e,t,r)=>ne(()=>{let n=r?.discard?void 0:[],o=e[Symbol.iterator](),s=o.next(),a=0;return Ar(No({while:()=>!s.done,body:()=>t(s.value,a++),step:i=>{n&&n.push(i),s=o.next()}}),n)}),pl=e=>e===\"unbounded\"?Number.POSITIVE_INFINITY:Math.max(1,e??1),km=()=>e=>{let t=e.onItem,r=e.step,n=(s,a,i,c,u)=>V(il(u),f=>r(s,a[i],f,i)??o(s,a,i+1,c)??pe),o=(s,a,i=0,c=a.length)=>{for(;i{let t=e.onItem,r=e.step;return(n,o,s)=>{let a=0,i=s.end??o.length,c=s.concurrency,u=!1,f,p,d,m,x,y=w=>{let E=Er(w);return m=E,u=!0,p&&p.size>0?V(ii(Oc(Array.from(p))),()=>m??E):E},S=()=>{let w=!1;for(;!m&&a{p.delete(_);try{if(m){if(b._tag===\"Failure\"){let T=b.cause.reasons.filter(C=>C._tag!==\"Interrupt\");if(T.length>0){let C=cs(T);m=Ht(m._tag===\"Failure\"?za(m.cause,C):C)}}}else{let T=r(n,E,b,h);T&&(m=T,S())}if(w){let T=S();T&&d(T)}else u&&p.size===0&&d(m??pe)}catch(T){d(y(T))}}),p.size{f=ds(),p=new Set,x=U,d=_;let h;try{h=S()}catch(b){return _(y(b))}return h?_(h):ne(()=>(m??=pr,V(p?Oc(p):pe,()=>m?._tag===\"Failure\"?m:pe)))})}if(u=!0,m){if(p&&p.size>0){let E=lm(f);p.forEach(U=>U.interruptUnsafe(f.id,E));return}if(d||m._tag===\"Failure\")return m}else if(d)if(p)p.size===0&&d(pe);else return pr};return S()}},vm=()=>e=>eO(e),Dz=eO({onItem(e,t,r){return e.f(t,r)},step(e,t,r,n){if(r._tag===\"Failure\")return r;e.out&&(e.out[n]=r.value)}}),BS=l(3,(e,t,r)=>V(e,n=>t(n)?D(n):r(n))),jS=l(3,(e,t,r)=>V(e,n=>{let o=t(n);return ce(o)?r(o.failure):D(o.success)})),tO=l(e=>ie(e[0]),(e,t,r)=>jS(e,t,r?n=>Qe(r(n)):()=>Qe(new Bn))),rO=l(e=>as(e[0])&&!ie(e[0]),(e,t,r)=>ne(()=>{let n=[];return Ar(tn(e,(o,s)=>{let a=t(o,s);return typeof a==\"boolean\"?(a&&n.push(o),pe):ke(a,i=>{i&&n.push(o)})},{discard:!0,concurrency:r?.concurrency}),n)})),nO=l(e=>as(e[0])&&!ie(e[0]),(e,t)=>ne(()=>{let r=[];for(let n of e){let o=t(n);mt(o)&&r.push(o.success)}return D(r)})),oO=l(e=>as(e[0])&&!ie(e[0]),(e,t,r)=>ne(()=>{let n=[];return Ar(tn(e,o=>ke(t(o),s=>{mt(s)&&n.push(s.success)}),{discard:!0,concurrency:r?.concurrency}),n)})),sO=D({}),aO=Ed(ke),iO=Id(ke,V),cO=bd(ke);var uO=l(e=>ie(e[0]),(e,t)=>re(r=>(Qz(),D(ra(r,e,t?.startImmediately,!1,t?.uninterruptible??!1))))),ra=(e,t,r=!1,n=!1,o=!1)=>{let s=e,a=o===\"inherit\"?s.interruptible:!o,i=new Xd(s.context,a);return n||(s.children().add(i),i._parent=s),r?i.evaluate(t):(i._asyncContext=mk(),s.currentDispatcher.scheduleTask(()=>i.evaluate(t),0)),i},fO=l(e=>ie(e[0]),(e,t)=>re(r=>D(ra(r,e,t?.startImmediately,!0,t?.uninterruptible)))),lO=e=>re(t=>{let r=t._children&&new Set(t._children),n=t.interruptible;return qn(e,o=>{let s=t._children;if(s===void 0||s.size===0)return pe;r&&(s=dA(s,i=>!r.has(i)));let a=nl(pm(s));return n?Am(a):a})}),zS=l(e=>ie(e[0]),(e,t,r)=>re(n=>{let o=ra(n,e,r?.startImmediately,!0,r?.uninterruptible);if(!o._exit)if(t.state._tag!==\"Closed\"){let s={};Lc(t,s,()=>Ik(i=>i===o.id?pe:rl(o))),o.addObserver(()=>tm(t,s))}else o.interruptUnsafe(n.id,lm(n));return D(o)})),pO=l(e=>ie(e[0]),(e,t)=>V(cl,r=>zS(e,r,t))),ys=e=>(t,r)=>{let n=new Xd(r?.scheduler?Jt(e,zf,r.scheduler):e,r?.uninterruptible!==!0);if(n.evaluate(t),n._exit)return n;if(r?.signal)if(r.signal.aborted)n.interruptUnsafe();else{let o=()=>n.interruptUnsafe();r.signal.addEventListener(\"abort\",o,{once:!0}),n.addObserver(()=>r.signal.removeEventListener(\"abort\",o))}return r?.onFiberStart&&r.onFiberStart(n),n},dO=l(2,(e,t)=>e._exit?e:t.state._tag===\"Closed\"?(e.interruptUnsafe(e.id),e):(tm(t,e),Lc(t,e,()=>rl(e)),e.addObserver(()=>tm(t,e)),e)),mO=ys(Ir()),qS=e=>{let t=ys(e);return(r,n)=>{let o=t(r,n);return n?.onExit&&o.addObserver(n.onExit),s=>o.interruptUnsafe(s)}},hO=qS(Ir()),Om=e=>{let t=ys(e);return(r,n)=>{let o=t(r,n);return new Promise(s=>{o.addObserver(a=>s(a))})}},xO=Om(Ir()),HS=e=>{let t=Om(e);return(r,n)=>t(r,n).then(o=>{if(o._tag===\"Failure\")throw um(o.cause);return o.value})},gO=HS(Ir()),_m=e=>{let t=ys(e);return r=>{if(Ne(r))return r;let n=new Qa(\"sync\"),o=t(r,{scheduler:n});return o._dispatcher?.flush(),o._exit??Er(new Xf(o))}},yO=_m(Ir()),WS=e=>{let t=_m(e);return r=>{let n=t(r);if(n._tag===\"Failure\")throw um(n.cause);return n.value}},SO=WS(Ir()),tk=D(!0),rk=D(!1),Wy=class{waiters=[];scheduled=void 0;_isOpen;constructor(t){this._isOpen=t}scheduleUnsafe(t){if(this.waiters.length===0)return tk;if(this.scheduled===void 0)this.scheduled=this.waiters,t.currentDispatcher.scheduleTask(this.flushScheduled,0);else for(let r=0;r{if(this.scheduled===void 0)return;let t=this.scheduled;this.scheduled=void 0;for(let r=0;rthis._isOpen?rk:(this._isOpen=!0,this.scheduleUnsafe(t)));release=re(t=>this._isOpen?rk:this.scheduleUnsafe(t));openUnsafe(){return this._isOpen?!1:(this._isOpen=!0,this.flushWaiters(),!0)}await=ht(t=>this._isOpen?t(pe):(this.waiters.push(t),z(()=>{let r=this.waiters.indexOf(t);r!==-1?this.waiters.splice(r,1):this.scheduled!==void 0&&(r=this.scheduled.indexOf(t),r!==-1&&this.scheduled.splice(r,1))})));closeUnsafe(){return this._isOpen?(this._isOpen=!1,!0):!1}close=z(()=>this.closeUnsafe());whenOpen=t=>V(this.await,()=>t);isOpen(){return this._isOpen}},JS=e=>new Wy(e??!1),bO=e=>z(()=>JS(e)),EO=re(e=>D(e.getRef(My))),IO=l(2,(e,t)=>kr(e,My,t)),TO=kr(Zd),wO=kr(ti),rm=BigInt(0),Lz={_tag:\"Span\",spanId:\"noop\",traceId:\"noop\",sampled:!1,status:{_tag:\"Ended\",startTime:rm,endTime:rm,exit:pr},attributes:new Map,links:[],kind:\"internal\",attribute(){},event(){},end(){},addLinks(){}},$z=e=>Object.assign(Object.create(Lz),e),CO=e=>e?Fe(e.annotations,Vd)?e._tag===\"Span\"?CO(qs(e.parent)):v():O(e):v(),KS=(e,t,r)=>{let n=!e.cache.tracerEnabled||r?.annotations&&Fe(r.annotations,Vd),o=r?.parent!==void 0?O(r.parent):r?.root?v():CO(e.cache.span),s;if(n)s=$z({name:t,parent:o,annotations:Jt(r?.annotations??Ir(),Vd,!0)});else{let a=e.cache.tracer??Fy,i=e.getRef(Nr),c=e.getRef(ti),u=e.getRef(Hf),f=e.getRef(Wf),p=r?.level??e.getRef(WA),d=r?.links!==void 0?[...f,...r.links]:f.length===0?[]:f.slice();s=a.span({name:t,parent:o,annotations:r?.annotations??Ir(),links:d,startTime:c?i.currentTimeNanosUnsafe():rm,kind:r?.kind??\"internal\",root:r?.root??!1,sampled:r?.sampled??(be(o)&&o.value.sampled===!1?!1:!qO(e.getRef(JA),p))});for(let m in u)s.attribute(m,u[m]);if(r?.attributes!==void 0)for(let m in r.attributes)s.attribute(m,r.attributes[m])}return s},AO=(e,t)=>re(r=>D(KS(r,e,t))),Jf=(e,t)=>ii(re(r=>{let n=_o(r.context,ea),o=KS(r,e,t??{}),s=r.getRef(Nr),a=r.getRef(ti);return Ar(ta(n,i=>Rm(o,i,s,a)),o)})),kO=function(){let e=typeof arguments[0]!=\"string\",t=e?arguments[1]:arguments[0],r=Gs(e?arguments[2]:arguments[1]);if(e){let n=arguments[0];return V(Jf(t,r),o=>_c(n,o,r))}return n=>V(Jf(t,r),o=>_c(n,o,r))},Bz=(e,t)=>(t=typeof t==\"function\"?t:bn,hs(qf,r=>({name:e,stack:t,parent:r}))),vO=Hf,OO=Wf,_O=l(e=>ie(e[0]),(e,t,r={})=>{let o=(Array.isArray(t)?t:[t]).map(s=>({span:s,attributes:r}));return hs(e,Wf,s=>[...s,...o])}),Rm=(e,t,r,n)=>z(()=>{e.status._tag!==\"Ended\"&&e.end(n?r.currentTimeNanosUnsafe():rm,t)}),Kf=(e,...t)=>{let r=t.length===1?void 0:t[0],n=t[t.length-1];return re(o=>{let s=KS(o,e,r),a=o.getRef(Nr),i=o.getRef(ti);return $c(o,c=>Rm(s,c,a,i)),n(s)})},Jy=kr(ei),_c=function(){let e=ie(arguments[0]),t=e?arguments[1]:arguments[0],r=e?arguments[2]:arguments[1],n=R;return t._tag===\"Span\"&&(r=Gs(r),n=Bz(t.name,r?.captureStackTrace)),e?Jy(n(arguments[0]),t):o=>Jy(n(o),t)},RO=function(){let e=typeof arguments[0]!=\"string\",t=e?arguments[1]:arguments[0],r=Gs(arguments[2]);if(e){let s=arguments[0];return Kf(t,arguments[2],a=>_c(s,a,r))}let n=typeof arguments[1]==\"function\"?arguments[1]:void 0,o=n?void 0:arguments[1];return(s,...a)=>Kf(t,n?n(...a):o,i=>_c(s,i,r))},PO=l(e=>ie(e[0]),(e,...t)=>hs(e,Hf,r=>{let n=t.length===1?{...r,...t[0]}:{...r};return t.length===1||M(n,t[0],t[1]),n})),MO=(...e)=>re(t=>{let r=t.currentSpanLocal;if(r)if(e.length===1)for(let[n,o]of Object.entries(e[0]))r.attribute(n,o);else r.attribute(e[0],e[1]);return pe}),FO=re(e=>{let t=e.currentSpanLocal;return t?D(t):Qe(new Bn)}),NO=Tz(ei),Nr=we(\"effect/Clock\",{defaultValue:()=>new Ky}),Ly=2**31-1,Ky=class{currentTimeMillisUnsafe(){return Date.now()}currentTimeMillis=z(()=>this.currentTimeMillisUnsafe());currentTimeNanosUnsafe(){return jz()}currentTimeNanos=z(()=>this.currentTimeNanosUnsafe());monotonicTimeNanosUnsafe(){return DO()}monotonicTimeNanos=z(()=>this.monotonicTimeNanosUnsafe());sleep(t){return this.sleepMillis(Cr(t))}sleepMillis(t){return t<=0?Mc:Number.isFinite(t)?ht(r=>{let n=t>Ly?this.sleepMillis(t-Ly):pe,o=setTimeout(()=>r(n),Math.min(t,Ly));return z(()=>clearTimeout(o))}):mm}},UO=BigInt(1e6),DO=(function(){let e=globalThis.process?.hrtime;if(typeof e?.bigint==\"function\")return()=>e.bigint();if(typeof performance<\"u\"&&typeof performance.now==\"function\")return()=>BigInt(Math.round(performance.now()*1e6));let t=BigInt(0);return()=>{let r=BigInt(Date.now())*UO;return r>t&&(t=r),t}})(),jz=(function(){let e=BigInt(1e9),t;return()=>{let r=DO(),n=BigInt(Date.now())*UO;if(t===void 0)t=n-r;else{let o=t+r;(n>o?n-o:o-n)>e&&(t=n-r)}return t+r}})(),Wn=e=>re(t=>e(t.getRef(Nr))),Bc=e=>Wn(t=>t.sleep(Ge(e))),GS=Wn(e=>e.currentTimeMillis);var Gf=\"~effect/Cause/TimeoutError\",LO=e=>k(e,Gf),Vf=class extends us(\"TimeoutError\"){[Gf]=Gf;constructor(t){super({message:t})}},Zf=\"~effect/Cause/IllegalArgumentError\",$O=e=>k(e,Zf),nm=class extends us(\"IllegalArgumentError\"){[Zf]=Zf;constructor(t){super({message:t})}},Yf=\"~effect/Cause/ExceededCapacityError\",BO=e=>k(e,Yf),om=class extends us(\"ExceededCapacityError\"){[Yf]=Yf;constructor(t){super({message:t})}},Qf=\"~effect/Cause/AsyncFiberError\",jO=e=>k(e,Qf),Xf=class extends us(\"AsyncFiberError\"){[Qf]=Qf;constructor(t){super({message:\"An asynchronous Effect was executed with Effect.runSync\",fiber:t})}},el=\"~effect/Cause/UnknownError\",zO=e=>k(e,el),Rc=class extends us(\"UnknownError\"){[el]=el;constructor(t,r){super({message:r,cause:t})}},VS=we(\"effect/Console\",{defaultValue:()=>globalThis.console}),zz=e=>{switch(e){case\"All\":return Number.MIN_SAFE_INTEGER;case\"Fatal\":return 5e4;case\"Error\":return 4e4;case\"Warn\":return 3e4;case\"Info\":return 2e4;case\"Debug\":return 1e4;case\"Trace\":return 0;case\"None\":return Number.MAX_SAFE_INTEGER}},qz=ay(fs,zz),qO=Tc(qz),ZS=we(\"effect/Logger/CurrentLoggers\",{defaultValue:()=>new Set([Zz,Yz])}),Hz=we(\"effect/Logger/LogToStderr\",{defaultValue:yf}),HO=function(){let e=typeof arguments[0]==\"string\"?[[arguments[0],arguments[1]]]:Object.entries(arguments[0]);return ii(re(t=>{let r=t.getRef(ps),n={...r};for(let o=0;o{let s=t.getRef(ps),a={...s};for(let i=0;i{let t=Object.create(Jz);return t.log=e,t},Kz=e=>e.replace(/[\\s=\"]/g,\"_\"),Gz=(e,t)=>`${Kz(e[0])}=${t-e[1]}ms`;var Jn=e=>(...t)=>{let r;for(let n=0,o=t.length;n{let o=e??n.cache.logLevel;if(qO(n.cache.minimumLogLevel,o))return pe;let s=n.getRef(Nr),a=n.getRef(ZS);if(a.size>0){let i=new Date(s.currentTimeMillisUnsafe());for(let c of a)c.log({cause:r,fiber:n,date:i,logLevel:o,message:t})}return pe})};var ri={bold:\"1\",red:\"31\",green:\"32\",yellow:\"33\",blue:\"34\",cyan:\"36\",white:\"37\",gray:\"90\",black:\"30\",bgBrightRed:\"101\"},kre={None:[],All:[],Trace:[ri.gray],Debug:[ri.blue],Info:[ri.green],Warn:[ri.yellow],Error:[ri.red],Fatal:[ri.bgBrightRed,ri.black]};var Vz=e=>`${e.getHours().toString().padStart(2,\"0\")}:${e.getMinutes().toString().padStart(2,\"0\")}:${e.getSeconds().toString().padStart(2,\"0\")}.${e.getMilliseconds().toString().padStart(3,\"0\")}`;var Zz=WO(({cause:e,date:t,fiber:r,logLevel:n,message:o})=>{let s=Array.isArray(o)?o.slice():[o];e.reasons.length>0&&s.push(fm(e));let a=t.getTime(),i=r.getRef(Yd),c=\"\";for(let d of i)c+=` ${Gz(d,a)}`;let u=r.getRef(ps);Object.keys(u).length>0&&s.push(u);let f=r.getRef(VS);(r.getRef(Hz)?f.error:f.log)(`[${Vz(t)}] ${n.toUpperCase()} (#${r.id})${c}:`,...s)}),Yz=WO(({cause:e,fiber:t,logLevel:r,message:n})=>{let o=t.getRef(Nr),s=t.getRef(ps),a=t.cache.span;if(a===void 0||a._tag===\"ExternalSpan\")return;let i={};for(let[c,u]of Object.entries(s))M(i,c,u);i[\"effect.fiberId\"]=t.id,i[\"effect.logLevel\"]=r.toUpperCase(),e.reasons.length>0&&(i[\"effect.cause\"]=fm(e)),a.event(IC(Array.isArray(n)&&n.length===1?n[0]:n),o.currentTimeNanosUnsafe(),i)});function Qz(){jy.interruptChildren??=xz}var YS=D(void 0);var JO=l(e=>ie(e[0]),(e,t)=>US(e,r=>re(n=>(Xz(n,r,t?.defectsOnly),pe)))),Xz=(e,t,r)=>{let n=e.getRef(VA);if(n.size===0||r&&!sm(t))return;let o={cause:t,fiber:e,timestamp:e.getRef(Nr).currentTimeNanosUnsafe()};n.forEach(s=>s.report(o))};var eq=Cf,tq=mc,eb=xc,tb=CC,na=jn,rb=qa,rq=Ha,oa=cs,nq=hd,oq=gc,ci=Xg,sq=Zs,jc=e=>new $a(e),zc=e=>new Af(e),nb=nk,ob=cm,Ss=fk,aq=za,Pm=um,sb=Gy,iq=Vy,ab=Ur,ib=ok,cq=sm,uq=sk,dl=tl,fq=am,lq=ak,pq=ik,dq=im,mq=Zy,cb=fm,hq=gd,xq=kf,gq=Bn,qc=vC,yq=Ba,ub=Wa,Y=_C,Sq=Gf,bq=LO,Eq=Vf,Iq=Zf,Tq=$O,ml=nm,wq=BO,Cq=Yf,fb=om,Aq=Qf,kq=jO,vq=Xf,Oq=el,_q=zO,Rq=Rc,Pq=yc,Mm=ck,Mq=uk,QS=class extends lr()(\"effect/Cause/StackTrace\"){},XS=class extends lr()(\"effect/Cause/InterruptorStackTrace\"){};var N={};Eo(N,{Do:()=>XH,Transaction:()=>Vc,TypeId:()=>WH,abortSignal:()=>yJ,acquireDisposable:()=>iJ,acquireRelease:()=>gi,acquireUseRelease:()=>Y_,addFinalizer:()=>cJ,all:()=>Ym,andThen:()=>_r,annotateCurrentSpan:()=>OJ,annotateLogs:()=>aK,annotateLogsScoped:()=>iK,annotateSpans:()=>vJ,as:()=>Tt,asSome:()=>eh,asVoid:()=>ia,awaitAllChildren:()=>qJ,bind:()=>rW,bindTo:()=>eW,cached:()=>dJ,cachedInvalidateWithTTL:()=>hJ,cachedWithTTL:()=>mJ,callback:()=>Yc,catch:()=>ca,catchCause:()=>rt,catchCauseFilter:()=>EW,catchCauseIf:()=>bW,catchDefect:()=>Wb,catchEager:()=>ru,catchFilter:()=>yW,catchIf:()=>gW,catchNoSuchElement:()=>SW,catchReason:()=>mW,catchReasons:()=>hW,catchTag:()=>pW,catchTags:()=>dW,clockWith:()=>Si,context:()=>hi,contextWith:()=>Ml,currentParentSpan:()=>RJ,currentSpan:()=>_J,delay:()=>Jb,die:()=>an,effectify:()=>yK,ensuring:()=>yi,eventually:()=>kW,exit:()=>vn,fail:()=>P,failCause:()=>ze,failCauseSync:()=>Is,failSync:()=>Xm,fiber:()=>HJ,fiberId:()=>WJ,filter:()=>Gb,filterMap:()=>qW,filterMapEffect:()=>Vb,filterMapOrElse:()=>WW,filterMapOrFail:()=>KW,filterOrElse:()=>HW,filterOrFail:()=>JW,findFirst:()=>VH,findFirstFilter:()=>ZH,firstSuccessOf:()=>UW,flatMap:()=>I,flatMapEager:()=>st,flatten:()=>qb,flip:()=>fW,fn:()=>QJ,fnUntraced:()=>Se,fnUntracedEager:()=>oE,forEach:()=>sa,forever:()=>$t,forkChild:()=>eu,forkDetach:()=>zJ,forkIn:()=>Ut,forkScoped:()=>tu,fromNullishOr:()=>iW,fromOption:()=>sW,fromResult:()=>Pl,gen:()=>dr,head:()=>YH,ignore:()=>RW,ignoreCause:()=>PW,interrupt:()=>_n,interruptible:()=>xJ,interruptibleMask:()=>gJ,isEffect:()=>jo,isFailure:()=>XW,isSuccess:()=>eJ,let:()=>tW,linkSpans:()=>FJ,log:()=>XJ,logDebug:()=>nK,logError:()=>nE,logFatal:()=>eK,logInfo:()=>rK,logTrace:()=>oK,logWarning:()=>tK,logWithLevel:()=>ih,makeSpan:()=>eE,makeSpanScoped:()=>NJ,map:()=>j,mapBoth:()=>IW,mapBothEager:()=>Vt,mapEager:()=>Dr,mapError:()=>th,mapErrorEager:()=>Ul,match:()=>Zb,matchCause:()=>VW,matchCauseEager:()=>ZW,matchCauseEffect:()=>zo,matchCauseEffectEager:()=>YW,matchEager:()=>GW,matchEffect:()=>QW,never:()=>mi,onError:()=>Vn,onErrorFilter:()=>fJ,onErrorIf:()=>uJ,onExit:()=>un,onExitFilter:()=>pJ,onExitIf:()=>lJ,onExitPrimitive:()=>Fl,onInterrupt:()=>Xb,option:()=>uW,orDie:()=>TW,orElseSucceed:()=>NW,partition:()=>JH,promise:()=>Qm,provide:()=>tJ,provideContext:()=>On,provideService:()=>qo,provideServiceEffect:()=>aJ,race:()=>zW,raceAll:()=>Kb,raceAllFirst:()=>jW,raceFirst:()=>Xc,reduce:()=>KH,repeat:()=>SJ,repeatOrElse:()=>bJ,replicate:()=>EJ,replicateEffect:()=>IJ,request:()=>BJ,requestUnsafe:()=>jJ,result:()=>cW,retry:()=>vW,retryOrElse:()=>OW,runCallback:()=>KJ,runCallbackWith:()=>JJ,runFork:()=>Nl,runForkWith:()=>Rn,runPromise:()=>tE,runPromiseExit:()=>ah,runPromiseExitWith:()=>GJ,runPromiseWith:()=>rE,runSync:()=>VJ,runSyncExit:()=>Wo,runSyncExitWith:()=>YJ,runSyncWith:()=>ZJ,sandbox:()=>_W,satisfiesErrorType:()=>bK,satisfiesServicesType:()=>EK,satisfiesSuccessType:()=>SK,schedule:()=>TJ,scheduleFrom:()=>Q_,scope:()=>xi,scoped:()=>oh,scopedWith:()=>sh,service:()=>Yb,serviceOption:()=>Qb,setContext:()=>rJ,sleep:()=>ua,spanAnnotations:()=>PJ,spanLinks:()=>MJ,succeed:()=>g,succeedNone:()=>aa,succeedSome:()=>zb,suspend:()=>$,sync:()=>X,tap:()=>Rr,tapCause:()=>V_,tapCauseFilter:()=>AW,tapCauseIf:()=>CW,tapDefect:()=>Z_,tapError:()=>rh,tapErrorTag:()=>wW,timed:()=>BW,timeout:()=>DW,timeoutOption:()=>LW,timeoutOrElse:()=>$W,tracer:()=>wJ,track:()=>uK,trackDefects:()=>pK,trackDuration:()=>dK,trackErrors:()=>lK,trackSuccesses:()=>fK,transposeOption:()=>aW,try:()=>Qc,tryPromise:()=>Zc,tx:()=>mK,txRetry:()=>gK,undefined:()=>QH,uninterruptible:()=>Ts,uninterruptibleMask:()=>Ho,unwrapReason:()=>xW,updateContext:()=>nJ,updateService:()=>oJ,updateServiceScoped:()=>sJ,useSpan:()=>UJ,validate:()=>GH,void:()=>Q,when:()=>nh,whileLoop:()=>Gn,withErrorReporting:()=>FW,withExecutionPlan:()=>MW,withFiber:()=>cn,withFiberSucceed:()=>oW,withLogSpan:()=>cK,withLogger:()=>sK,withParentSpan:()=>$J,withSpan:()=>DJ,withSpanScoped:()=>LJ,withTracer:()=>CJ,withTracerEnabled:()=>AJ,withTracerTiming:()=>kJ,yieldNow:()=>Rl,yieldNowWith:()=>nW,zip:()=>Hb,zipWith:()=>lW});var ir={};Eo(ir,{asVoid:()=>Wq,asVoidAll:()=>hb,die:()=>lb,fail:()=>bs,failCause:()=>sn,filterCause:()=>$q,filterFailure:()=>Lq,filterSuccess:()=>Uq,filterValue:()=>Dq,findDefect:()=>jq,findError:()=>Bq,findErrorOption:()=>Gq,getCause:()=>Kq,getSuccess:()=>Jq,hasDies:()=>Nq,hasFails:()=>Fq,hasInterrupts:()=>db,interrupt:()=>pb,isExit:()=>hl,isFailure:()=>Uo,isSuccess:()=>Ve,map:()=>zq,mapBoth:()=>Hq,mapError:()=>qq,match:()=>mb,succeed:()=>vr,void:()=>xt});var hl=vf,vr=Ae,sn=Ht,bs=en,lb=Er,pb=Nc,xt=pr;var Ve=Qs,Uo=lS,Fq=Wk,Nq=Hk,db=dS,Uq=Bk,Dq=jk,Lq=zk,$q=ol,Bq=pS,jq=qk,mb=Kk,zq=mS,qq=hS,Hq=xS,Wq=Gk,hb=gS,Jq=Vk,Kq=Zk,Gq=Yk;var Vq=\"~effect/Deferred\";var Zq={[Vq]:{_A:R,_E:R},pipe(){return Z(this,arguments)}},KO=function(){this.resumes=void 0,this.effect=void 0};KO.prototype=Zq;var ui=()=>new KO;var Fm=e=>ht(t=>e.effect?t(e.effect):(e.resumes??=[],e.resumes.push(t),z(()=>{let r=e.resumes;if(r===void 0)return;let n=r.indexOf(t);n>=0&&r.splice(n,1)})));var Yq=l(2,(e,t)=>z(()=>Do(e,t))),Qq=Yq;var Xq=l(2,(e,t)=>Qq(e,Ht(t)));var xb=l(2,(e,t)=>Xq(e,Zs(t)));var Do=(e,t)=>{if(e.effect)return!1;if(e.effect=t,e.resumes){let r=e.resumes;e.resumes=void 0;for(let n=0;n{let r={observers:0,deferred:ui(),scope:kn(),finalizer:n=>ne(()=>--r.observers>0?pe:(e.map.delete(t),Le(r.scope,n)))};return r},QO=(e,t)=>t.state._tag===\"Closed\"?!1:(e.observers++,Lc(t,{},e.finalizer),!0),t_=e=>k(e,e_),rH={[e_]:{_ROut:R,_E:R,_RIn:R},pipe(){return Z(this,arguments)}},r_=e=>{let t=Object.create(rH);return t.build=e,t},gb=e=>r_((t,r)=>{let n=Nt(r);return on(e(t,n),o=>o._tag===\"Failure\"?Le(n,o):pe)}),nH=e=>{let t=gb((r,n)=>r.getOrElseMemoize(t,n,e));return t},Um=class{get[YO](){return YO}parent;constructor(t){this.parent=t}map=new Map;get(t,r){let n=this.map.get(t);return n?(QO(n,r),n.deferred.effect??Fm(n.deferred)):this.parent?.get(t,r)}getOrElseMemoize(t,r,n){return ne(()=>{let o;return qn(ne(()=>{let s=this.get(t,r);if(s)return s;let a=tH(this,t);return QO(a,r)?(o=a.deferred,this.map.set(t,a),n(this,a.scope)):n(this,r)}),s=>{o&&Do(o,s)})})}},gl=()=>new Um,oH=e=>new Um(e);var xl=class e extends lr()(\"effect/Layer/CurrentMemoMap\"){static forkOrCreate(t){let r=Fd(t,e);return r?oH(r):gl()}},yl=l(3,(e,t,r)=>kr(ke(ne(()=>e.build(t,r)),Jt(xl,t)),xl,t));var Lm=l(2,(e,t)=>re(r=>yl(e,xl.forkOrCreate(r.context),t))),n_=function(){return arguments.length===1?e=>Dm(Za(arguments[0],e)):Dm(Za(arguments[0],arguments[1]))},Dm=e=>r_(je(D(e)));var yb=function(){return arguments.length===1?e=>XO(arguments[0],e):XO(arguments[0],arguments[1])},XO=(e,t)=>sH(ke(t,r=>Za(e,r))),sH=e=>nH((t,r)=>Es(e,r));var o_=(e,t,r)=>{let n=Nt(r,\"parallel\");return tn(e,o=>o.build(t,Nt(n,\"sequential\")),{concurrency:e.length}).pipe(ke(o=>aA(...o)))},s_=(...e)=>gb((t,r)=>o_(e,t,r));var aH=(e,t,r)=>gb((n,o)=>V(Array.isArray(t)?o_(t,n,o):t.build(n,o),s=>e.build(n,o).pipe(rn(s),ke(a=>r(a,s))))),a_=l(2,(e,t)=>aH(e,t,R));var Hc=Nr;var $m=\"~effect/DateTime\",Bm=\"~effect/DateTime/TimeZone\",c_={[$m]:$m,pipe(){return Z(this,arguments)},[Te](){return this.toString()},toJSON(){return Il(this).toJSON()}},cH={...c_,_tag:\"Utc\",[se](){return It(this.epochMilliseconds)},[ae](e){return Jc(e)&&e._tag===\"Utc\"&&this.epochMilliseconds===e.epochMilliseconds},toString(){return`DateTime.Utc(${Il(this).toJSON()})`}},uH={...c_,_tag:\"Zoned\",[se](){return le(It(this.epochMilliseconds))(H(this.zone))},[ae](e){return Jc(e)&&e._tag===\"Zoned\"&&this.epochMilliseconds===e.epochMilliseconds&&J(this.zone,e.zone)},toString(){return`DateTime.Zoned(${_b(this)})`}},u_={[Bm]:Bm,[Te](){return this.toString()}},fH={...u_,_tag:\"Named\",[se](){return Re(`Named:${this.id}`)},[ae](e){return li(e)&&e._tag===\"Named\"&&this.id===e.id},toString(){return`TimeZone.Named(${this.id})`},toJSON(){return{_id:\"TimeZone\",_tag:\"Named\",id:this.id}}},lH={...u_,_tag:\"Offset\",[se](){return Re(`Offset:${this.offset}`)},[ae](e){return li(e)&&e._tag===\"Offset\"&&this.offset===e.offset},toString(){return`TimeZone.Offset(${kb(this.offset)})`},toJSON(){return{_id:\"TimeZone\",_tag:\"Offset\",offset:this.offset}}},$o=(e,t,r)=>{let n=Object.create(uH);return n.epochMilliseconds=e,n.zone=t,Object.defineProperty(n,\"partsUtc\",{value:r,enumerable:!1,writable:!0}),Object.defineProperty(n,\"adjustedEpochMillis\",{value:void 0,enumerable:!1,writable:!0}),Object.defineProperty(n,\"partsAdjusted\",{value:void 0,enumerable:!1,writable:!0}),n},Jc=e=>k(e,$m);var li=e=>k(e,Bm),f_=e=>li(e)&&e._tag===\"Offset\",l_=e=>li(e)&&e._tag===\"Named\",p_=e=>e._tag===\"Utc\",Eb=e=>e._tag===\"Zoned\",d_=nr((e,t)=>e.epochMilliseconds===t.epochMilliseconds);var m_=e=>{let t=Object.create(cH);return t.epochMilliseconds=e,Object.defineProperty(t,\"partsUtc\",{value:void 0,enumerable:!1,writable:!0}),t},Wc=e=>{let t=e.getTime();if(Number.isNaN(t))throw new ml(\"Invalid date\");return m_(t)},zm=e=>{if(Jc(e))return e;if(e instanceof Date)return Wc(e);if(typeof e==\"object\"){if(\"epochMilliseconds\"in e)return Wc(new Date(e.epochMilliseconds));let t=new Date(0);return SH(t,e),Wc(t)}else if(typeof e==\"string\"&&!pH(e))return Wc(new Date(e+\"Z\"));return Wc(new Date(e))},pH=e=>/Z|GMT|[+-]\\d{2}$|[+-]\\d{2}:?\\d{2}$|\\]$/.test(e),dH=-864e13+720*60*1e3,mH=864e13-840*60*1e3,Ib=(e,t)=>{let r=t?.timeZone;if(r===void 0&&Jc(e)&&Eb(e))return e;let n=zm(e);if(n.epochMillisecondsmH)throw new RangeError(`Epoch millis out of range: ${n.epochMilliseconds}`);r===void 0&&typeof e==\"object\"&&\"timeZoneId\"in e&&(r=e.timeZoneId);let o;if(r===void 0){let s=new Date(n.epochMilliseconds).getTimezoneOffset()*-60*1e3;o=El(s)}else if(li(r))o=r;else if(typeof r==\"number\")o=El(r);else{let s=Cb(r);if(fe(s))throw new ml(`Invalid time zone: ${r}`);o=s.value}return t?.adjustForTimeZone!==!0?$o(n.epochMilliseconds,o,n.partsUtc):bH(n.epochMilliseconds,o,t?.disambiguation??\"compatible\")},Sb=zn(Ib),h_=zn(zm),hH=/^(.{17,35})\\[(.+)\\]$/,x_=e=>{let t=hH.exec(e);if(t===null){let o=Ob(e);return o!==null?Sb(e,{timeZone:o}):v()}let[,r,n]=t;return Sb(r,{timeZone:n})};var g_=e=>m_(e.epochMilliseconds);var jm=new Map,xH={day:\"numeric\",month:\"numeric\",year:\"numeric\",hour:\"numeric\",minute:\"numeric\",second:\"numeric\",timeZoneName:\"longOffset\",fractionalSecondDigits:3,hourCycle:\"h23\"},gH=e=>{let t=e.resolvedOptions().timeZone,r=jm.get(t);if(r!==void 0)return r;let n=Object.create(fH);return n.id=t,n.format=e,jm.set(t,n),n},Tb=e=>{let t=jm.get(e);if(t!==void 0)return t;try{let r=gH(new Intl.DateTimeFormat(\"en-US\",{...xH,timeZone:e}));return jm.set(e,r),r}catch{throw new ml(`Invalid time zone: ${e}`)}},El=e=>{let t=Object.create(lH);return t.offset=e,t},wb=zn(Tb);var yH=/^(?:GMT|[+-])/,Cb=e=>{if(yH.test(e)){let t=Ob(e);return t===null?v():O(El(t))}return wb(e)},y_=e=>e._tag===\"Offset\"?kb(e.offset):e.id;var Il=e=>new Date(e.epochMilliseconds),qm=e=>{if(e._tag===\"Utc\")return new Date(e.epochMilliseconds);if(e.zone._tag===\"Offset\")return new Date(e.epochMilliseconds+e.zone.offset);if(e.adjustedEpochMilliseconds!==void 0)return new Date(e.adjustedEpochMilliseconds);let t=e.zone.format.formatToParts(e.epochMilliseconds).filter(n=>n.type!==\"literal\"),r=new Date(0);return r.setUTCFullYear(Number(t[2].value),Number(t[0].value)-1,Number(t[1].value)),r.setUTCHours(Number(t[3].value),Number(t[4].value),Number(t[5].value),t[6]?.type===\"fractionalSecond\"?Number(t[6].value):(e.epochMilliseconds%1e3+1e3)%1e3),e.adjustedEpochMilliseconds=r.getTime(),r},Ab=e=>qm(e).getTime()-vb(e),kb=e=>{let t=Math.abs(e),r=Math.floor(t/(3600*1e3)),n=Math.round(t%(3600*1e3)/(60*1e3));return n===60&&(r+=1,n=0),`${e<0?\"-\":\"+\"}${String(r).padStart(2,\"0\")}:${String(n).padStart(2,\"0\")}`},S_=e=>kb(Ab(e)),vb=e=>e.epochMilliseconds;var SH=(e,t)=>{if((t.year!==void 0||t.month!==void 0||t.day!==void 0)&&e.setUTCFullYear(t.year??e.getUTCFullYear(),t.month!==void 0?t.month-1:e.getUTCMonth(),t.day??e.getUTCDate()),t.weekDay!==void 0){let r=t.weekDay-e.getUTCDay();e.setUTCDate(e.getUTCDate()+r)}t.hour!==void 0&&e.setUTCHours(t.hour),t.minute!==void 0&&e.setUTCMinutes(t.minute),t.second!==void 0&&e.setUTCSeconds(t.second),t.millisecond!==void 0&&e.setUTCMilliseconds(t.millisecond)};var i_=1440*60*1e3,bH=(e,t,r)=>{if(t._tag===\"Offset\")return $o(e-t.offset,t);let n=bl(e-i_,e,t),o=bl(e+i_,e,t);if(n===o)return $o(e-n,t);let s=n{let t=EH.exec(e);if(t===null)return null;let[,r,n,o]=t;return(r===\"+\"?1:-1)*(Number(n)*60+Number(o))*60*1e3},bl=(e,t,r)=>{let n=r.format.formatToParts(e).find(s=>s.type===\"timeZoneName\")?.value??\"\";if(n===\"GMT\")return 0;let o=Ob(n);return o===null?Ab($o(t,r)):o};var b_=e=>Il(e).toISOString();var bb=e=>{let t=qm(e);return e._tag===\"Utc\"?t.toISOString():`${t.toISOString().slice(0,-1)}${S_(e)}`},_b=e=>e.zone._tag===\"Offset\"?bb(e):`${bb(e)}[${e.zone.id}]`;var zre=globalThis.Number;var I_=l(2,(e,t)=>{if(!globalThis.Number.isFinite(e)||!globalThis.Number.isFinite(t)||t===0)return NaN;if(globalThis.Number.isInteger(e)&&globalThis.Number.isInteger(t))return e%t;let[r,n]=E_(e),[o,s]=E_(t),a=Math.min(n,s),i=r*BigInt(10)**BigInt(n-a),c=o*BigInt(10)**BigInt(s-a),u=i%c;if(u===BigInt(0))return e<0||Object.is(e,-0)?-0:0;let f=globalThis.Number(`${u}e${a}`);return f===0?Math.sign(e)*globalThis.Number.MIN_VALUE:f});function E_(e){if(globalThis.Number.isInteger(e))return[BigInt(e),0];let t=Math.abs(e).toExponential(),r=t.indexOf(\"e\"),n=t.slice(0,r).replace(\".\",\"\");return[BigInt(n)*(e<0?-BigInt(1):BigInt(1)),globalThis.Number(t.slice(r+1))-n.length+1]}var T_=e=>{let t=Math.ceil(Math.log(e)/Math.log(2));return Math.max(Math.pow(2,t),2)};var Hre=globalThis.String,Hm=br;var w_=e=>e.toUpperCase();var C_=e=>e.trim();var wH=(e,t,r,n,o)=>{let s=e;for(let c of t)s=s.replace(c,\"$1\\0$2\");for(let c of r)s=s.replace(c,\"\\0\");let a=0,i=s.length;for(;s.charAt(a)===\"\\0\";)a++;for(;s.charAt(i-1)===\"\\0\";)i--;return s.slice(a,i).split(\"\\0\").map(o).join(n)};var CH=[/([a-z0-9])([A-Z])/g,/([A-Z])([A-Z][a-z])/g],AH=/[^A-Z0-9]+/gi;var A_=e=>wH(e,CH,[AH],\"_\",w_);var He=l(2,(e,t)=>xs(e,kH,r=>t(r))),Bo=e=>e.reasons.some(Rb),Rb=e=>e._tag===\"Fail\"&&qc(e.error),Tl=e=>{let t,r=!1;for(let n of e.reasons)Rb(n)?t??=n.error:n._tag!==\"Interrupt\"&&(r=!0);return t===void 0?W(e):r?W(oa(e.reasons.filter(n=>!Rb(n)))):B(t)};var kH=e=>{let t=Tl(e);return ce(t)?t:B(t.success.value)},Jm=e=>{let t=Tl(e);return ce(t)?sn(t.failure):vr(t.success.value)},Km=l(2,(e,t)=>Mo(e,{onSuccess:t.onSuccess,onFailure:r=>{let n=Tl(r);return ce(n)?t.onFailure(n.failure):t.onDone(n.success.value)}}));var k_=\"~effect/Schedule\";var Gt=we(\"effect/Schedule/CurrentMetadata\",{defaultValue:je({input:void 0,output:void 0,duration:Ws,attempt:0,start:0,now:0,elapsed:0,elapsedSincePrevious:0})}),vH={[k_]:{_Out:R,_In:R,_Env:R},pipe(){return Z(this,arguments)}},Pb=e=>k(e,k_),wl=e=>{let t=Object.create(vH);return t.step=e,t},Mb=()=>{let e=0,t,r;return(n,o)=>{r===void 0&&(r=n);let s=n-r,a=t===void 0?0:n-t;return t=n,{input:o,attempt:++e,start:r,now:n,elapsed:s,elapsedSincePrevious:a}}},OH=e=>wl(ke(e,t=>{let r=Mb();return(n,o)=>t(r(n,o))})),di=e=>nn(e.step,t=>D(()=>De(t))),Kn=e=>Wn(t=>ke(di(e),r=>{let n=Mb();return o=>ne(()=>{let s=t.currentTimeMillisUnsafe();return V(r(s,o),([a,i])=>{let c=n(s,o);return c.output=a,c.duration=i,Ar(Bc(i),c)})})})),Cl=e=>ke(Kn(e),t=>r=>ke(t(r),n=>n.output));var Fb=e=>wl(ke(di(e),t=>(r,n)=>Km(t(r,n),{onSuccess:o=>D([n,o[1]]),onFailure:De,onDone:()=>Y(n)}))),v_=e=>Al(kl,({attempt:t})=>D(t<=e)),Nb=e=>{let t=Ge(e);return OH(D(r=>D([r.attempt-1,t])))};var Al=l(2,(e,t)=>wl(ke(di(e),r=>{let n=Mb();return(o,s)=>V(r(o,s),a=>{let[i,c]=a,u=t({...n(o,s),output:i,duration:c});return V(ie(u)?u:D(u),f=>f?D(a):Y(i))})})));var kl=Nb(Ws);var O_=\"~effect/ExecutionPlan\";var _H={[O_]:O_,get captureRequirements(){let e=this;return Xs(t=>D(RH(e.steps.map(r=>({...r,while:r.while?n=>rn(r.while(n),t):void 0,schedule:r.schedule?wl(rn(ke(di(r.schedule),n=>(o,s)=>rn(n(o,s),t)),t)):void 0,provide:t_(r.provide)?a_(r.provide,Dm(t)):r.provide})))))},pipe(){return Z(this,arguments)}},RH=e=>{let t=Object.create(_H);return t.steps=e,t};var Gm=we(\"effect/ExecutionPlan/CurrentMetadata\",{defaultValue:je({attempt:0,stepIndex:0})});var PH=(e,t,r)=>Cm(n=>V(r?.local?yl(t,gl(),n):Lm(t,n),o=>rn(e,o))),Vm=l(e=>ie(e[0]),(e,t,r)=>Uf(t)?rn(e,t):PH(e,Array.isArray(t)?s_(...t):t,r));var R_=e=>e.reasons.every(jn)?Ur(e):W(e),Db=l(3,(e,t,r)=>V(Kn(t),n=>{let o=Gt.defaultValue();return xs(He(ym(si(V(ne(()=>kr(e,Gt,o)),n),s=>z(()=>{o=s})),{disableYield:!0}),s=>D(s)),R_,s=>r(s,o.attempt===0?v():O(o)))})),Lb=l(3,(e,t,r)=>V(Kn(t),n=>{let o=Gt.defaultValue(),s,a=xs(ne(()=>kr(e,Gt,o)),R_,i=>(s=i,V(n(i),c=>(o=c,a))));return He(a,i=>js(()=>r(s,i)))})),P_=l(2,(e,t)=>{let r=typeof t==\"function\"?t(R):Pb(t)?t:Ol(t);return Db(e,r,Qe)}),vl=l(2,(e,t)=>{let r=typeof t==\"function\"?t(R):Pb(t)?t:Ol(t);return Lb(e,r,Qe)}),M_=l(3,(e,t,r)=>V(Kn(r),n=>{let o=Gt.defaultValue(),s=ne(()=>kr(e,Gt,o));return He(V(n(t),a=>{o=a;let i=je(V(s,n));return No({while:Sr,body:i,step(c){o=c}})}),a=>D(a))})),MH=Fb(kl),Ol=e=>{let t=e.schedule?Fb(e.schedule):MH;return e.while&&(t=Al(t,({input:r})=>{let n=e.while(r);return ie(n)?n:D(n)})),e.until&&(t=Al(t,({input:r})=>{let n=e.until(r);return ie(n)?ke(n,o=>!o):D(!n)})),e.times!==void 0&&(t=Al(t,({attempt:r})=>D(r<=e.times))),t};var $b=(e,t)=>{let r=-1,n=0,o=s=>Em(e(s));return{begin:Wn(s=>ne(()=>{let a=t();a.stepIndex!==r&&(r=a.stepIndex,n=0),n++;let i={attempt:a.attempt,stepAttempt:n,stepIndex:a.stepIndex,startNanos:s.monotonicTimeNanosUnsafe()};return Ar(o({_tag:\"AttemptStart\",attempt:i.attempt,stepAttempt:i.stepAttempt,stepIndex:i.stepIndex}),i)})),end:(s,a)=>Wn(i=>{let c=Ro(i.monotonicTimeNanosUnsafe()-s.startNanos);return o(a._tag===\"Success\"?{_tag:\"AttemptSuccess\",attempt:s.attempt,stepAttempt:s.stepAttempt,stepIndex:s.stepIndex,duration:c}:{_tag:\"AttemptFailure\",attempt:s.attempt,stepAttempt:s.stepAttempt,stepIndex:s.stepIndex,duration:c,cause:a.cause})})}},N_=l(e=>ie(e[0]),(e,t,r)=>ne(()=>{let n=0,o={attempt:0,stepIndex:0},s=gm(Gm,z(()=>(o={attempt:o.attempt+1,stepIndex:n},o))),a=r?.onEvent===void 0?void 0:$b(r.onEvent,()=>o),i=a===void 0?R:u=>Hn(f=>V(a.begin,p=>on(f(u),d=>a.end(p,d)))),c;return V(No({while:()=>np?d:(p=!0,Pc(c))),f=vl(f,_l(u,!1))}else{let p=_l(u,!0);f=p?vl(f,p):f}return Vs(f)},step(u){c=u,n++}}),()=>Pc(c))})),_l=(e,t)=>{if(t){if(e.attempts===1||!(e.schedule||e.attempts))return}else return Ol({schedule:e.schedule?e.schedule:e.attempts?void 0:FH,times:e.attempts,while:e.while});return Ol({schedule:e.schedule,while:e.while,times:e.attempts?e.attempts-1:void 0})},FH=v_(1);var NH=\"~effect/Request\",UH=gC({_E:e=>e,_A:e=>e,_R:e=>e}),cne={...wC,[NH]:UH};var D_=e=>e;var $_=l(2,(e,t)=>{let r=n=>ht(o=>{let s=j_(n,e,o,ds());return LH(n,s)});return ie(t)?V(t,r):r(t)}),B_=(e,t)=>{let r=j_(t.resolver,e,t.onExit,{context:t.context,cache:{scheduler:Fe(t.context,zf)}});return()=>z_(t.resolver,r)},Zm=[],Bb=new WeakMap,j_=(e,t,r,n)=>{let o=Bb.get(e);o||(o=new Map,Bb.set(e,o));let s,a=!1,i=D_({request:t,context:n.context,uninterruptible:!1,completeUnsafe(u){a||(a=!0,!(s&&!s.entrySet.delete(i))&&r(u))}});if(e.preCheck!==void 0&&!e.preCheck(i))return i;let c=e.batchKey(i);if(s=o.get(c),!s){if(Zm.length>0)s=Zm.pop(),s.key=c,s.resolver=e,s.map=o;else{let u={key:c,resolver:e,map:o,entrySet:new Set,entries:new Set,delayEffect:V(ne(()=>u.resolver.delay),f=>L_(u)),run:on(ne(()=>u.resolver.runAll(Array.from(u.entries),u.key)),f=>{for(let p of u.entrySet)p.completeUnsafe(f._tag===\"Success\"?Er(new Error(\"Effect.request: RequestResolver did not complete request\",{cause:p.request})):f);return u.entries.clear(),Zm.length<128&&(u.entrySet.clear(),u.key=void 0,u.fiber=void 0,u.resolver=void 0,u.map=void 0,Zm.push(u)),pe})};s=u}o.set(c,s),s.fiber=ys(n.context)(s.delayEffect,{scheduler:n.cache.scheduler})}return s.entrySet.add(i),s.entries.add(i),s.resolver.collectWhile(s.entries)||(s.fiber.interruptUnsafe(n.id),s.fiber=ys(n.context)(L_(s),{scheduler:n.cache.scheduler})),i},z_=(e,t)=>{if(t.uninterruptible)return;let r=Bb.get(e);if(!r)return;let n=e.batchKey(t),o=r.get(n);if(!o||!o.entries.delete(t))return;o.entrySet.delete(t);let s;o.entries.size===0&&(r.delete(n),s=o.fiber),t.completeUnsafe(Nc()),s?.interruptUnsafe()},LH=(e,t)=>z(()=>z_(e,t));function L_(e){return e.map.has(e.key)?(e.map.delete(e.key),e.run):pe}var BH=\"effect/Metric/CurrentMetricAttributes\",jH=we(BH,{defaultValue:()=>({})}),zH=\"effect/Metric/MetricRegistry\",qH=we(zH,{defaultValue:()=>new Map}),q_=\"~effect/Metric\",H_=class{[q_]=q_;#t;#e=\"\";id;description;attributes;constructor(t,r,n){this.id=t,this.description=r,this.attributes=n}valueUnsafe(t){return this.hook(t).get(t)}modifyUnsafe(t,r){return this.hook(r).modify(t,r)}updateUnsafe(t,r){return this.hook(r).update(t,r)}hook(t){let r=Fe(t,jH);r!==this.#t&&(this.#t=r,this.#e=HH(this,W_(this.attributes,r)));let n=Fe(t,qH),o=n.get(this.#e);return aC(o)&&(o={id:this.id,type:this.type,description:this.description,attributes:Object.keys(r).length>0?W_(this.attributes,r):this.attributes,hooks:this.createHooks()},n.set(this.#e,o)),o.hooks}pipe(){return Z(this,arguments)}};var Gc=l(2,(e,t)=>Xs(r=>z(()=>e.updateUnsafe(t,r))));function HH(e,t){let r=Object.entries(t).sort((n,o)=>n[0]o[0]?1:0);return JSON.stringify([e.type,e.id,e.description,r])}function W_(e,t){return{...J_(e),...J_(t)}}function J_(e){return Io(e)&&Array.isArray(e)?e.reduce((t,[r,n])=>(M(t,r,n),t),{}):e}var WH=ko,jo=ie,Ym=ll,JH=$S,KH=Kv,GH=Gv,VH=Vv,ZH=Yv,sa=tn,YH=Xv,Gn=No,Qm=nS,Zc=Ek,g=D,aa=oi,zb=rS,$=ne,X=z,Q=pe;var QH=YS;var Yc=ht,mi=mm,XH=sO,eW=aO,tW=cO;var rW=iO,dr=Ck,P=Qe,Xm=dm,ze=De,Is=Sk,an=Ys,Qc=bk;var Rl=Mc,nW=tS,cn=re,oW=kC,Pl=Pc,sW=eS,aW=yk,iW=gk,I=V,qb=Nk,_r=Fo,Rr=si,cW=Vs,uW=bv,vn=il,j=ke,Tt=Ar,eh=Fc,ia=nl,fW=_k,Hb=rv,lW=SS,ca=gs;var pW=TS,dW=dv,mW=mv,hW=hv,xW=xv,rt=nn,Wb=cv,gW=sl,yW=bm,SW=iv,bW=ES,EW=xs,th=wS,IW=CS,TW=AS,rh=IS,wW=lv,V_=uv,CW=fv,AW=Sm,Z_=pv,kW=kS,vW=vl,OW=Lb,_W=Rk,RW=Sv,PW=Em,MW=N_,FW=JO,NW=gv,UW=yv,DW=kv,LW=_S,$W=OS,Jb=Av,ua=Bc,BW=vv,Kb=cS,jW=uS,zW=Pk,Xc=fS,Gb=rO,qW=nO,Vb=oO,HW=BS,WW=jS,JW=nv,KW=tO,nh=ov,Zb=Im,GW=al,VW=vS,ZW=Tv,YW=Mk,zo=Mo,QW=Dc,XW=wv,eJ=Cv,hi=yS,Ml=Xs,tJ=Vm,On=rn,rJ=tv,Yb=Qk,Qb=Xk,nJ=Uc,oJ=hs,sJ=ev,qo=kr,aJ=gm,xi=cl,oh=PS,sh=Cm,gi=ai,iJ=Lv,Y_=ul,cJ=MS,yi=Nv,Vn=US,uJ=Uv,fJ=DS,Fl=qn,un=on,lJ=FS,pJ=NS,dJ=jv,mJ=Bv,hJ=LS,_n=zv,xJ=Am,Xb=Dv,Ts=ii,Ho=Hn,gJ=Wv,yJ=Jv,$t=ym,SJ=P_,bJ=Db,EJ=bS,IJ=sv,TJ=l(2,(e,t)=>Q_(e,void 0,t)),Q_=M_,wJ=EO,CJ=IO,AJ=TO,kJ=wO,vJ=PO,OJ=MO,_J=FO,RJ=NO,PJ=vO,MJ=OO,FJ=_O,eE=AO,NJ=Jf,UJ=Kf,DJ=RO,LJ=kO,$J=_c,BJ=$_,jJ=B_,eu=uO,Ut=zS,tu=pO,zJ=fO,qJ=lO,HJ=Tk,WJ=wk,Nl=mO,Rn=ys,JJ=qS,KJ=hO,tE=gO,rE=HS,ah=xO,GJ=Om,VJ=SO,ZJ=WS,Wo=yO,YJ=_m,Se=sS,QJ=Ak,Si=Wn,ih=Jn,XJ=Jn(),eK=Jn(\"Fatal\"),tK=Jn(\"Warn\"),nE=Jn(\"Error\"),rK=Jn(\"Info\"),nK=Jn(\"Debug\"),oK=Jn(\"Trace\"),sK=l(2,(e,t)=>hs(e,ZS,r=>new Set([...r,t]))),aK=l(e=>jo(e[0]),(e,...t)=>hs(e,GO,r=>{let n=t.length===1?{...r,...t[0]}:{...r};return t.length===1||M(n,t[0],t[1]),n})),iK=HO,cK=l(2,(e,t)=>V(GS,r=>hs(e,VO,n=>[[t,r],...n]))),uK=l(e=>jo(e[0]),(e,t,r)=>un(e,n=>{let o=r===void 0?n:r(n);return Gc(t,o)})),fK=l(e=>jo(e[0]),(e,t,r)=>Rr(e,n=>{let o=r===void 0?n:r(n);return Gc(t,o)})),lK=l(e=>jo(e[0]),(e,t,r)=>rh(e,n=>{let o=r===void 0?n:r(n);return Gc(t,o)})),pK=l(e=>jo(e[0]),(e,t,r)=>Z_(e,n=>{let o=r===void 0?n:r(n);return Gc(t,o)})),dK=l(e=>jo(e[0]),(e,t,r)=>Si(n=>{let o=n.monotonicTimeNanosUnsafe();return un(e,()=>{let s=n.monotonicTimeNanosUnsafe(),a=PA(Ge(s),Ge(o)),i=r===void 0?a:r(a);return Gc(t,i)})})),Vc=class extends lr()(\"effect/Effect/Transaction\"){},mK=e=>cn(t=>{let r=Fd(t.context,Vc);if(r&&!G_.has(r))return e;r={journal:new Map,retry:!1};let n;return Ho(o=>I(Gn({while:()=>!n,body:je(o(e).pipe(qo(Vc,r),V_(s=>r.retry?cm(s)?o(hK(r)):X(()=>jb(r)):Q),vn)),step(s){if(r.retry||!X_(r))return jb(r);Ve(s)&&xK(t,r),jb(r),G_.add(r),n=s}}),()=>n))}),G_=new WeakSet,X_=e=>{for(let[t,{version:r}]of e.journal)if(t.version!==r)return!1;return!0},hK=e=>Yc(t=>{if(!X_(e))return t(Q);let r={},n=Array.from(e.journal.keys()),o=()=>{for(let a of n)a.pending.delete(r)},s=()=>{o(),t(Q)};for(let a of n)a.pending.set(r,s);return X(o)});function xK(e,t){for(let[r,{value:n,written:o}]of t.journal)if(o){Object.is(n,r.value)||(r.version=r.version+1,r.value=n);for(let s of r.pending.values())e.currentDispatcher.scheduleTask(s,0);r.pending.clear()}}function jb(e){e.retry=!1,e.journal.clear()}var gK=I(Vc,e=>(e.retry=!0,_n)),yK=(e,t,r)=>(...n)=>Yc(o=>{try{e(...n,(s,a)=>{o(s?t?$(()=>P(t(s,n))):P(s):g(a))})}catch(s){o(r?P(r(s,n)):an(s))}}),SK=()=>e=>e,bK=()=>e=>e,EK=()=>e=>e,Dr=Uk,Ul=Dk,Vt=Lk,st=Fk,ru=$k,oE=kk;var eR=/^[+-]?\\d+$/,aE=\"~effect/BigDecimal\",IK={[aE]:aE,[se](){let e=cE(this);return le(Re(String(e.value)),It(e.scale))},[ae](e){return iE(e)&&aR(this,e)===0},toString(){return`BigDecimal(${ws(this)})`},toJSON(){return{_id:\"BigDecimal\",value:String(this.value),scale:this.scale}},[Te](){return this.toJSON()},pipe(){return Z(this,arguments)}},iE=e=>k(e,aE),nR=(e,t)=>{if(!Number.isSafeInteger(t))throw new RangeError(`Scale must be a safe integer, got ${t}`);let r=Object.create(IK);return r.value=e,r.scale=t,r},oR=(e,t)=>{let r=nR(e,t);return r.normalized=r,r};var Zn=BigInt(0),TK=BigInt(1);var wK=BigInt(10),sR=oR(Zn,0);var cE=e=>{if(e.normalized===void 0)if(e.value===Zn)e.normalized=sR;else{let t=`${e.value}`,r=t.length;for(;t[r-1]===\"0\";)r--;e.normalized=oR(BigInt(t.slice(0,r)),e.scale-(t.length-r))}return e.normalized};var CK=100,AK=[TK],sE=(e,t)=>e===t?0:e{let r=`${e.value{if(e.scale===t.scale)return sE(e.value,t.value);let r=rR(e),n=rR(t);if(r!==n)return rCK)return r===-1?tR(t,e):tR(e,t);let a=AK[s]??=wK**BigInt(s);return o>0?sE(e.value,t.value*a):sE(e.value*a,t.value)},nu=zs(aR);var rR=e=>e.value===Zn?0:e.value{if(e===\"\")return O(sR);let t,r,n=e.search(/[eE]/);if(n!==-1){let c=e.slice(n+1);if(t=e.slice(0,n),r=Number(c),t===\"\"||!Number.isSafeInteger(r)||!eR.test(c))return v()}else t=e,r=0;let o,s,a=t.search(/\\./);if(a!==-1){let c=t.slice(0,a),u=t.slice(a+1);o=`${c}${u}`,s=u.length}else o=t,s=0;if(!eR.test(o))return v();let i=s-r;return Number.isSafeInteger(i)?O(nR(BigInt(o),i)):v()};var ws=e=>{let t=cE(e);if(Math.abs(t.scale)>=16)return kK(t);let r=t.value0?n.padStart(t.scale+1,\"0\"):n.padEnd(n.length-t.scale,\"0\"),s=o.length-t.scale,a=t.scale>0?`${o.slice(0,s)}.${o.slice(s)}`:o;return r?`-${a}`:a},kK=e=>{if(vK(e))return\"0e+0\";let t=cE(e),r=`${t.value}`,n=t.value=0?\"+\":\"\"}${a}`};var vK=e=>e.value===Zn;var me={};Eo(me,{Any:()=>tV,Array:()=>$e,ArrayEnsure:()=>pV,BigDecimal:()=>pD,BigDecimalFromString:()=>JZ,BigInt:()=>Bu,BigIntFromString:()=>_Z,Boolean:()=>XT,BooleanFromBit:()=>DZ,BooleanLiterals:()=>nw,ByteSize:()=>Ju,ByteSizeFromBigInt:()=>GZ,ByteSizeFromNumber:()=>VZ,ByteSizeFromString:()=>KZ,Cause:()=>VT,CauseReason:()=>GT,Char:()=>hZ,Chunk:()=>ZZ,Class:()=>zD,Cookie:()=>$D,Cookies:()=>BD,Date:()=>Wu,DateFromMillis:()=>TZ,DateFromString:()=>IZ,DateTimeUtc:()=>Rx,DateTimeUtcFromDate:()=>r5,DateTimeUtcFromMillis:()=>o5,DateTimeUtcFromString:()=>n5,DateTimeZoned:()=>OD,DateTimeZonedFromString:()=>c5,Defect:()=>gZ,Duration:()=>Mx,DurationFromMillis:()=>Y5,DurationFromNanos:()=>Z5,DurationFromString:()=>hw,Enum:()=>X3,Error:()=>qD,ErrorInstance:()=>xZ,Exit:()=>Q5,FalseLiterals:()=>jU,File:()=>sD,Finite:()=>qu,FiniteFromString:()=>OZ,FormData:()=>aD,Graph:()=>r8,HashMap:()=>n8,HashSet:()=>o8,Headers:()=>u8,InetAddress:()=>DD,InetAddressFromString:()=>q5,InetAddressV4:()=>j5,InetAddressV6:()=>z5,Int:()=>lo,IpAddress:()=>Ki,IpAddressFromString:()=>Gu,IpInterface:()=>MD,IpInterfaceFromString:()=>D5,IpLinkLocalAddress:()=>T5,IpLinkLocalAddressFromString:()=>w5,IpLoopbackAddress:()=>E5,IpLoopbackAddressFromString:()=>I5,IpMulticastAddress:()=>u5,IpMulticastAddressFromString:()=>f5,IpNetwork:()=>UD,IpNetworkFromString:()=>B5,IpUnicastAddress:()=>d5,IpUnicastAddressFromString:()=>m5,IpUnspecifiedAddress:()=>C5,IpUnspecifiedAddressFromString:()=>A5,Ipv4Address:()=>Px,Ipv4AddressFromString:()=>dw,Ipv4BroadcastAddress:()=>g5,Ipv4BroadcastAddressFromString:()=>y5,Ipv4Interface:()=>RD,Ipv4InterfaceFromString:()=>N5,Ipv4Network:()=>FD,Ipv4NetworkFromString:()=>L5,Ipv4PrivateAddress:()=>k5,Ipv4PrivateAddressFromString:()=>v5,Ipv6Address:()=>mw,Ipv6AddressFromString:()=>_D,Ipv6Interface:()=>PD,Ipv6InterfaceFromString:()=>U5,Ipv6Network:()=>ND,Ipv6NetworkFromString:()=>$5,Ipv6UniqueLocalAddress:()=>O5,Ipv6UniqueLocalAddressFromString:()=>_5,Json:()=>Nx,JsonFromUrlParamsField:()=>f8,JsonObject:()=>L8,Literal:()=>Ct,Literals:()=>fo,MacAddress:()=>Ji,MacAddressFromString:()=>Ku,MacBroadcastAddress:()=>S5,MacBroadcastAddressFromString:()=>b5,MacLocallyAdministeredAddress:()=>R5,MacLocallyAdministeredAddressFromString:()=>P5,MacMulticastAddress:()=>l5,MacMulticastAddressFromString:()=>p5,MacUnicastAddress:()=>h5,MacUnicastAddressFromString:()=>x5,MacUniversallyAdministeredAddress:()=>M5,MacUniversallyAdministeredAddressFromString:()=>F5,MutableJson:()=>B8,Natural:()=>zi,Never:()=>eV,NonEmptyArray:()=>tw,NonEmptyString:()=>fw,Null:()=>wx,NullOr:()=>rw,NullishOr:()=>PU,Number:()=>Wi,NumberFromString:()=>vZ,ObjectKeyword:()=>oV,Opaque:()=>CV,Option:()=>Gi,OptionFromNullOr:()=>s8,OptionFromNullishOr:()=>i8,OptionFromOptional:()=>d8,OptionFromOptionalKey:()=>p8,OptionFromOptionalNullOr:()=>m8,OptionFromUndefinedOr:()=>a8,PropertyKey:()=>KT,ReadonlyMap:()=>yZ,ReadonlySet:()=>SZ,Record:()=>_s,RecordFromCookies:()=>c8,RecordFromUrlParams:()=>l8,Redacted:()=>Fx,RedactedFromValue:()=>h8,RegExp:()=>bZ,Result:()=>x8,SchemaError:()=>Os,SocketAddress:()=>W5,StandardSchemaV1FailureResult:()=>UZ,String:()=>K,StringForLiteralAutocomplete:()=>rV,StringFromBase64:()=>PZ,StringFromBase64Url:()=>MZ,StringFromHex:()=>FZ,StringFromUriComponent:()=>NZ,Struct:()=>Me,StructWithRest:()=>fV,Symbol:()=>OU,TaggedClass:()=>b8,TaggedError:()=>E8,TaggedStruct:()=>kx,TaggedUnion:()=>wV,TemplateLiteral:()=>Y3,TemplateLiteralParser:()=>Q3,TimeZone:()=>kD,TimeZoneFromString:()=>i5,TimeZoneNamed:()=>CD,TimeZoneNamedFromString:()=>a5,TimeZoneOffset:()=>s5,Tree:()=>D8,Trim:()=>RZ,Trimmed:()=>cD,TrueLiterals:()=>BU,Tuple:()=>Qo,TupleWithRest:()=>lV,URL:()=>Ox,URLFromString:()=>EZ,URLSearchParams:()=>iD,Uint8Array:()=>_x,Uint8ArrayFromBase64:()=>LZ,Uint8ArrayFromBase64Url:()=>$Z,Uint8ArrayFromHex:()=>BZ,Undefined:()=>QT,UndefinedOr:()=>Ax,Union:()=>it,UniqueArray:()=>dV,UniqueSymbol:()=>sV,UnixPathAddress:()=>LD,UnixPathAddressFromString:()=>H5,Unknown:()=>vp,UnknownFromJsonString:()=>CZ,UrlParams:()=>xw,Void:()=>nV,annotate:()=>w3,annotateEncoded:()=>C3,annotateKey:()=>A3,asserts:()=>R3,brand:()=>NU,catchDecoding:()=>gV,catchDecodingWithContext:()=>LU,catchEncoding:()=>yV,catchEncodingWithContext:()=>$U,check:()=>hV,declare:()=>Be,declareConstructor:()=>qr,decode:()=>SV,decodeEffect:()=>P3,decodeExit:()=>M3,decodeOption:()=>N3,decodePromise:()=>D3,decodeResult:()=>U3,decodeSync:()=>L3,decodeTo:()=>G,decodeUnknownEffect:()=>Ex,decodeUnknownExit:()=>bU,decodeUnknownOption:()=>F3,decodeUnknownPromise:()=>IU,decodeUnknownResult:()=>EU,decodeUnknownSync:()=>TU,encode:()=>bV,encodeEffect:()=>$3,encodeExit:()=>B3,encodeKeys:()=>cV,encodeOption:()=>z3,encodePromise:()=>H3,encodeResult:()=>q3,encodeSync:()=>W3,encodeTo:()=>ow,encodeUnknownEffect:()=>Ix,encodeUnknownExit:()=>wU,encodeUnknownOption:()=>j3,encodeUnknownPromise:()=>AU,encodeUnknownResult:()=>CU,encodeUnknownSync:()=>kU,extendTo:()=>uV,fieldsAssign:()=>aV,flip:()=>JT,fromBrand:()=>xV,fromFormData:()=>AZ,fromJsonString:()=>lw,fromURLSearchParams:()=>kZ,instanceOf:()=>ba,is:()=>_3,isBase64:()=>KU,isBase64Url:()=>FV,isBetween:()=>Hu,isBetweenBigDecimal:()=>WZ,isBetweenBigInt:()=>oZ,isBetweenCodePoints:()=>iZ,isBetweenDate:()=>eZ,isBetweenLength:()=>iw,isBetweenProperties:()=>dZ,isBetweenSize:()=>fZ,isCapitalized:()=>BV,isEndingWith:()=>UV,isFinite:()=>zV,isGUID:()=>PV,isGreaterThan:()=>HV,isGreaterThanBigDecimal:()=>jZ,isGreaterThanBigInt:()=>tZ,isGreaterThanDate:()=>ZV,isGreaterThanOrEqualTo:()=>GU,isGreaterThanOrEqualToBigDecimal:()=>zZ,isGreaterThanOrEqualToBigInt:()=>VU,isGreaterThanOrEqualToDate:()=>YV,isIncluding:()=>DV,isInt:()=>vx,isInt32:()=>GV,isLessThan:()=>WV,isLessThanBigDecimal:()=>qZ,isLessThanBigInt:()=>rZ,isLessThanDate:()=>QV,isLessThanOrEqualTo:()=>JV,isLessThanOrEqualToBigDecimal:()=>HZ,isLessThanOrEqualToBigInt:()=>nZ,isLessThanOrEqualToDate:()=>XV,isLowercased:()=>$V,isMaxCodePoints:()=>aZ,isMaxLength:()=>XU,isMaxProperties:()=>pZ,isMaxSize:()=>uZ,isMinCodePoints:()=>sZ,isMinLength:()=>ZU,isMinProperties:()=>lZ,isMinSize:()=>cZ,isMultipleOf:()=>KV,isNonEmpty:()=>QU,isPattern:()=>zu,isPropertyNames:()=>mZ,isSchema:()=>Tx,isSchemaError:()=>hU,isStartingWith:()=>NV,isStringBigInt:()=>vV,isStringFinite:()=>kV,isStringSymbol:()=>OV,isTrimmed:()=>JU,isULID:()=>MV,isUUID:()=>RV,isUint32:()=>VV,isUncapitalized:()=>jV,isUnique:()=>uw,isUniqueKey:()=>eD,isUppercased:()=>LV,link:()=>Oe,linkDecoding:()=>ju,make:()=>q,makeFilter:()=>Ue,makeFilterGroup:()=>AV,makeIsBetween:()=>Mp,makeIsGreaterThan:()=>Op,makeIsGreaterThanOrEqualTo:()=>_p,makeIsLessThan:()=>Rp,makeIsLessThanOrEqualTo:()=>Pp,makeIsMultipleOf:()=>qV,middlewareDecoding:()=>UU,middlewareEncoding:()=>DU,mutable:()=>mV,mutableKey:()=>G3,optional:()=>Lu,optionalKey:()=>qi,overrideToCodecIso:()=>N8,overrideToEquivalence:()=>w8,overrideToFormatter:()=>I8,readonlyKey:()=>V3,refine:()=>FU,required:()=>K3,requiredKey:()=>J3,resolveAnnotations:()=>j8,resolveAnnotationsKey:()=>z8,revealBottom:()=>T3,revealCodec:()=>k3,suspend:()=>MU,tag:()=>Hi,tagDefaultOmit:()=>TV,toCodecArrayFromSingle:()=>_8,toCodecIso:()=>O8,toCodecJson:()=>v8,toCodecStringTree:()=>Np,toDifferJsonPatch:()=>U8,toEncoded:()=>$u,toEncoderXml:()=>R8,toEquivalence:()=>C8,toFormatter:()=>T8,toIso:()=>P8,toIsoFocus:()=>F8,toIsoSource:()=>M8,toJsonSchemaDocument:()=>k8,toRepresentation:()=>A8,toStandardJSONSchemaV1:()=>O3,toStandardSchemaV1:()=>v3,toTaggedUnion:()=>WU,toType:()=>Hr,withArrayLengthConstraints:()=>Cx,withConstructorDefault:()=>zU,withDecodingDefault:()=>HU,withDecodingDefaultKey:()=>sw,withDecodingDefaultType:()=>IV,withDecodingDefaultTypeKey:()=>EV});var uR=globalThis.BigInt;var fR=e=>e>uR(Number.MAX_SAFE_INTEGER)||ee.names.map(t=>[t,e])));var fE=e=>e;var ou=e=>{throw new Error(`Invalid ByteSize: ${e}`)},dR=e=>!Number.isSafeInteger(e)||e<0?ou(`expected a non-negative safe integer, received ${e}`):fE(BigInt(e));var mR=e=>{let t=/^\\s*(\\d+)(?:\\.(\\d+))?\\s*([A-Za-z]+)\\s*$/.exec(e);if(t===null)return ou(`unsupported syntax ${JSON.stringify(e)}`);let r=PK.get(t[3]);if(r===void 0)return ou(`unsupported unit ${JSON.stringify(t[3])}`);let n=t[2]??\"\",o=BigInt(10)**BigInt(n.length),s=BigInt(t[1]+n)*r.factor;return s%o!==uE?ou(`${JSON.stringify(e)} does not represent an integral number of bytes`):fE(s/o)},hR=zn(mR),ch=e=>{switch(typeof e){case\"bigint\":return etypeof e==\"bigint\"?ch(e):dR(e);var gR=e=>typeof e==\"bigint\"&&e>=uE;var uh=e=>e,yR=fR;var bR=\"~effect/Chunk\";function SR(e,t,r,n,o){for(let s=t;snr((t,r)=>{if(t.length!==r.length)return!1;let n=bi(t),o=bi(r);for(let s=0;se},toString(){return`Chunk(${F(bi(this))})`},toJSON(){return{_id:\"Chunk\",values:vt(bi(this))}},[Te](){return this.toJSON()},[ae](e){return lh(e)&&NK(this,e)},[se](){return To(bi(this))},[Symbol.iterator](){switch(this.backing._tag){case\"IArray\":return this.backing.array[Symbol.iterator]();case\"IEmpty\":return ER[Symbol.iterator]();default:return bi(this)[Symbol.iterator]()}},pipe(){return Z(this,arguments)}},mE=e=>{let t=Object.create(UK);switch(t.backing=e,e._tag){case\"IEmpty\":{t.length=0,t.depth=0,t.left=t,t.right=t;break}case\"IConcat\":{t.length=e.left.length+e.right.length,t.depth=1+Math.max(e.left.depth,e.right.depth),t.left=e.left,t.right=e.right;break}case\"IArray\":{t.length=e.array.length,t.depth=0,t.left=Cs,t.right=Cs;break}case\"ISingleton\":{t.length=1,t.depth=0,t.left=Cs,t.right=Cs;break}case\"ISlice\":{t.length=e.length,t.depth=0,t.left=Cs,t.right=Cs;break}}return t},lh=e=>k(e,bR),Cs=mE({_tag:\"IEmpty\"}),DK=()=>Cs;var LK=e=>mE({_tag:\"ISingleton\",a:e}),hE=e=>lh(e)?e:BK(Ye(e)),pE=(e,t,r)=>{switch(e.backing._tag){case\"IArray\":{SR(e.backing.array,0,t,r,e.length);break}case\"IConcat\":{pE(e.left,t,r),pE(e.right,t,r+e.left.length);break}case\"ISingleton\":{t[r]=e.backing.a;break}case\"ISlice\":{SR(bi(e.backing.chunk),e.backing.offset,t,r,e.length);break}}};var $K=e=>{switch(e.backing._tag){case\"IEmpty\":return ER;case\"IArray\":return e.backing.array;default:{let t=new Array(e.length);return pE(e,t,0),e.backing={_tag:\"IArray\",array:t},e.left=Cs,e.right=Cs,e.depth=0,t}}},bi=$K;var BK=e=>e.length===0?DK():e.length===1?LK(e[0]):mE({_tag:\"IArray\",array:e});var IR=e=>e.length;var gE=Jc,TR=li,wR=f_,CR=l_,AR=p_,kR=Eb,vR=d_;var OR=zm;var _R=Ib;var yE=h_,RR=x_;var SE=g_;var bE=Tb,EE=El,PR=wb;var MR=Cb,Dl=y_;var FR=Il;var NR=vb;var UR=b_;var IE=_b;var su=e=>jK(typeof e==\"string\"?zK.encode(e):e),Ei=e=>{let t=HK(e),r=t.length;if(r%4!==0)return W(new An({kind:\"Decode\",module:\"Base64\",input:t,message:`Length must be a multiple of 4, but is ${r}`}));let n=t.indexOf(\"=\");if(n!==-1&&(n$f(u4(e,t,r));function f4(e){let t=aP(e.name,e.valueEncoded,e.options);if(t!==void 0)throw t;let r=e.name+\"=\"+e.valueEncoded;if(e.options===void 0)return r;let n=e.options;if(n.maxAge!==void 0){let o=vA(Ge(n.maxAge));r+=\"; Max-Age=\"+Math.trunc(o)}if(n.domain!==void 0&&(r+=\"; Domain=\"+n.domain),n.path!==void 0&&(r+=\"; Path=\"+n.path),n.priority!==void 0)switch(n.priority){case\"low\":r+=\"; Priority=Low\";break;case\"medium\":r+=\"; Priority=Medium\";break;case\"high\":r+=\"; Priority=High\";break}if(n.expires!==void 0&&(r+=\"; Expires=\"+n.expires.toUTCString()),n.httpOnly&&(r+=\"; HttpOnly\"),n.secure&&(r+=\"; Secure\"),n.partitioned&&(r+=\"; Partitioned\"),n.sameSite!==void 0)switch(n.sameSite){case\"lax\":r+=\"; SameSite=Lax\";break;case\"strict\":r+=\"; SameSite=Strict\";break;case\"none\":r+=\"; SameSite=None\";break}return r}var cP=e=>{let t={},r=Object.values(e.cookies);for(let n=0;nObject.values(e.cookies).map(f4);var l4=e=>{try{return decodeURIComponent(e)}catch{return e}};var Ai=new WeakMap,fP=e=>{if(Ai.has(e))return Ai.get(e);throw new Error(\"Unable to get redacted value\"+(e.label?` with label: \"${e.label}\"`:\"\"))};var lP=\"~effect/Redacted\",qE=e=>k(e,lP),ki=(e,t)=>{let r=Object.create(m4);return t?.label&&(r.label=t.label),Ai.set(r,e),r},m4={[lP]:{_A:e=>e},label:void 0,...rr,toJSON(){return this.toString()},toString(){return``},[se](){return H(Ai.get(this))},[ae](e){return qE(e)&&J(Ai.get(this),Ai.get(e))}},au=fP;var pP=e=>nr((t,r)=>e(au(t),au(r)));var WE=Symbol.for(\"~effect/http/Headers\"),dP=e=>k(e,WE),JE=Object.defineProperties(Object.create(null),{[WE]:{value:WE},[Na]:{value(e){return x4(this,Fe(e,g4))}},toJSON:{value(){return is(this)}},[ae]:{value(e){return KE(this,e)}},[se]:{value(){return Tf(this)}},toString:{value:dc.toString},[Te]:{value:dc[Te]}});var KE=hA(Ja()),h4=Object.create(JE),mP=e=>{if(e===void 0)return h4;if(Symbol.iterator in e){let r=Object.create(JE);for(let[n,o]of e)r[n.toLowerCase()]=o;return r}let t=Object.create(JE);for(let[r,n]of Object.entries(e))Array.isArray(n)?t[r.toLowerCase()]=n.join(\", \"):n!==void 0&&(t[r.toLowerCase()]=n);return t};var x4=l(2,(e,t)=>{let r={...e},n=o=>{if(typeof o==\"string\"){let s=o.toLowerCase();s in e&&(r[s]=ki(e[s]))}else for(let s in e)s.search(o)!==-1&&(r[s]=ki(e[s]))};if(Array.isArray(t))for(let o=0;o[\"authorization\",\"cookie\",\"set-cookie\",\"x-api-key\",\"AWSAccessKeyId\",\"Signature\",\"sig\",\"X-Goog-Signature\"]});var GE=\"~effect/http/UrlParams\",VE=e=>k(e,GE),S4={...rr,[GE]:GE,[Symbol.iterator](){return this.params[Symbol.iterator]()},toJSON(){return{_id:\"UrlParams\",params:Object.fromEntries(this.params)}},[ae](e){return ZE(this,e)},[se](){return To(this.params.flat())}},Sh=e=>{let t=Object.create(S4);return t.params=e,t},hP=e=>{if(VE(e))return e;let t=xP(e),r=[];for(let n=0;n{let t=typeof e[Symbol.iterator]==\"function\"?Ye(e):Object.entries(e),r=[];for(let[n,o]of t)if(Array.isArray(o))for(let s=0;sb4(e.params,t.params)),b4=SA(cA([Ja(),Ja()]));var gP=l(2,(e,t)=>yy(e.params,([r])=>r===t).pipe(Rt(([,r])=>r)));var E4=e=>{let t={};for(let[r,n]of e.params)if(!Object.hasOwn(t,r))M(t,r,n);else{let o=t[r];typeof o==\"string\"?M(t,r,[o,n]):o.push(n)}return t},yP=E4;var YE=\"~effect/Graph\",QE=e=>k(e,YE),SP=e=>e;var T4=(e,t,r)=>(e===\"directed\"?t.source===r.source&&t.target===r.target:t.source===r.source&&t.target===r.target||t.source===r.target&&t.target===r.source)&&J(t.data,r.data),w4=(e,t)=>e===\"directed\"?H(t):qt(le(H(t.data),C4(t.source,t.target))),C4=(e,t)=>le(0,H(e))+le(0,H(t)),A4=Re(\"Graph\"),k4={[YE]:{_N:e=>e,_E:e=>e},[Symbol.iterator](){return this.nodes[Symbol.iterator]()},[Te](){return this.toJSON()},[ae](e){if(k(e,YE)){let t=SP(e);if(this.nodes.size!==t.nodes.size||this.edges.size!==t.edges.size||this.type!==t.type)return!1;for(let[r,n]of this.nodes)if(!t.nodes.has(r)||!J(n,t.nodes.get(r)))return!1;for(let[r,n]of this.edges){let o=t.edges.get(r);if(o===void 0||!T4(this.type,n,o))return!1}return!0}return!1},[se](){let e=A4;e=e^Re(this.type),e=e^It(this.nodes.size),e=e^It(this.edges.size);for(let[t,r]of this.nodes)e^=le(H(t),H(r));for(let[t,r]of this.edges)e^=le(H(t),w4(this.type,r));return qt(e)},toJSON(){return{_id:\"Graph\",nodeCount:this.nodes.size,edgeCount:this.edges.size,type:this.type}},toString(){return`Graph(${this.type}, ${this.nodes.size}, ${this.edges.size})`},pipe(){return Z(this,arguments)}},v4=(e,t)=>{let r=Object.create(k4);return r.type=e,r.mutable=t,r.transforming=!1,r.nodes=new Map,r.edges=new Map,r.adjacency=new Map,r.reverseAdjacency=new Map,r.nextNodeIndex=0,r.nextEdgeIndex=0,r.acyclic=O(!0),r};var Hl=e=>{let t=SP(e);return{type:e.type,nodes:Array.from(t.nodes,([r,n])=>({index:r,data:n})).sort((r,n)=>r.index-n.index),edges:Array.from(t.edges,([r,n])=>({index:r,source:n.source,target:n.target,data:n.data})).sort((r,n)=>r.index-n.index)}},bh=e=>{let t=v4(e.type,!1);for(let r of e.nodes)t.nodes.set(r.index,r.data),t.adjacency.set(r.index,[]),t.reverseAdjacency.set(r.index,[]);for(let r of e.edges)t.edges.set(r.index,{source:r.source,target:r.target,data:r.data}),t.adjacency.get(r.source).push(r.index),t.reverseAdjacency.get(r.target).push(r.index),e.type===\"undirected\"&&(t.adjacency.get(r.target).push(r.index),t.reverseAdjacency.get(r.source).push(r.index));return t.nextNodeIndex=e.nodes.length===0?0:e.nodes[e.nodes.length-1].index+1,t.nextEdgeIndex=e.edges.length===0?0:e.edges[e.edges.length-1].index+1,t.acyclic=v(),t};function As(e){return e.checks?e.checks[e.checks.length-1].annotations:e.annotations}function Eh(e){return t=>As(t)?.[e]}var Lr=\"~structural\",vi=\"~identifier\",Wl=\"~sentinels\",Ih=\"~constructor\",bP=[\"title\",\"description\",\"default\",\"examples\",\"readOnly\",\"writeOnly\",\"format\",\"contentEncoding\",\"contentMediaType\",\"contentSchema\"],iu=Eh(\"identifier\"),XE=Eh(vi),eI=Eh(\"title\");var Jl=Vr(e=>{let t=As(e)?.identifier;return typeof t==\"string\"?t:e.getExpected(Jl)}),EP=new Set([Wl,Lr,\"representation\",\"arbitraryConstraint\",\"toJsonSchema\",\"toCode\",\"toEquivalence\",\"toFormatter\",\"toCodec\",\"toCodecJson\",\"toCodecStringTree\",\"toCodecIso\",\"toCodecArbitrary\"]);var ge=Symbol();var Zt=vr,$r=Zt(ge),qe=Zt(ge),Kl=e=>e===ge?v():O(e),tI=e=>e._tag===\"None\"?$r:Zt(e.value);var Zl=\"~effect/SchemaIssue/Issue\";function nI(e){return k(e,Zl)&&e[Zl]===Zl}function _4(e){return Object.hasOwn(e,\"input\")}var fn=class{[Zl]=Zl;constructor(t,r){r?.reportInput===!0&&t!==ge&&(this.input=t)}},IP=class extends fn{_tag=\"Filter\";filter;issue;constructor(e,t,r,n){super(r,n),this.filter=e,this.issue=t}},TP=class extends fn{_tag=\"Encoding\";ast;issue;constructor(e,t,r,n){super(r,n),this.ast=e,this.issue=t}},ut=class extends fn{_tag=\"Pointer\";path;issue;constructor(e,t){super(),this.path=e,this.issue=t}},ma=class extends fn{_tag=\"MissingKey\";annotations;constructor(e){super(),this.annotations=e}},oI=class extends fn{_tag=\"UnexpectedKey\";ast;constructor(e,t,r){super(t,r),this.ast=e}},wt=class extends fn{_tag=\"Composite\";ast;issues;constructor(e,t,r,n){super(r,n),this.ast=e,this.issues=t}},ft=class extends fn{_tag=\"InvalidType\";ast;constructor(e,t,r){super(t,r),this.ast=e}},de=class extends fn{_tag=\"InvalidValue\";annotations;constructor(e,t,r){super(t,r),this.annotations=e}};function Br(e,t,r,n,o){return new wt(e,[new ut([t],r)],n,o)}var Yl=class extends fn{_tag=\"Forbidden\";annotations;constructor(e,t,r){super(t,r),this.annotations=e}},Ql=class extends fn{_tag=\"AnyOf\";ast;issues;constructor(e,t,r,n){super(r,n),this.ast=e,this.issues=t}},wP=class extends fn{_tag=\"OneOf\";ast;successes;constructor(e,t,r,n){super(r,n),this.ast=e,this.successes=t}};function rI(e,t,r){if(nI(e))return e;if(typeof e==\"string\")return new de({message:e},t,r);let n=typeof e.issue==\"string\"?new de({message:e.issue},t,r):e.issue;return new ut(e.path,n)}function CP(e,t,r){if(e!==void 0)return typeof e==\"boolean\"?e?void 0:new de(void 0,t,r):rI(e,t,r)}function AP(e,t,r,n){return Array.isArray(t)?tt(t)?t.length===1?rI(t[0],r,n):new wt(e,wn(t,o=>rI(o,r,n)),r,n):void 0:CP(t,r,n)}var kP=e=>{let t=uu(e);if(t!==void 0)return t;switch(e._tag){case\"InvalidType\":return da(Jl(e.ast),e);case\"InvalidValue\":{let r=sI(e);if(r!==void 0)return da(r,e);let n=Th(e);return n===void 0?\"Expected a valid value\":`Invalid data ${n}`}case\"MissingKey\":return\"Missing key\";case\"UnexpectedKey\":{let r=Th(e);return r===void 0?\"Expected no excess property\":`Unexpected key with value ${r}`}case\"Forbidden\":return\"Forbidden operation\";case\"OneOf\":{let r=Th(e);return r===void 0?\"Expected exactly one member to match\":`Expected exactly one member to match the input ${r}`}}},vP=e=>uu(e.issue)??uu(e);function OP(e){return t=>({issues:cu(t,[],e?.leafHook??kP,e?.checkHook??vP)})}function Th(e){return _4(e)?F(e.input):void 0}function sI(e){let t=e.annotations?.expected;return typeof t==\"string\"?t:void 0}function da(e,t){let r=Th(t);return r===void 0?`Expected ${e}`:`Expected ${e}, got ${r}`}function cu(e,t,r,n){switch(e._tag){case\"Filter\":{let o=n(e);if(o!==void 0)return[{path:t,message:o}];if(e.issue._tag!==\"InvalidValue\")return cu(e.issue,t,r,n);let s=sI(e.issue);return[{path:t,message:s===void 0?da(aI(e.filter),e):da(s,e.issue)}]}case\"Encoding\":return cu(e.issue,t,r,n);case\"Pointer\":return cu(e.issue,[...t,...e.path],r,n);case\"Composite\":return e.issues.flatMap(o=>cu(o,t,r,n));case\"AnyOf\":return e.issues.length===0?[{path:t,message:uu(e)??da(Jl(e.ast),e)}]:e.issues.flatMap(o=>cu(o,t,r,n));default:return[{path:t,message:r(e)}]}}function aI(e){let t=e.annotations?.expected;if(typeof t==\"string\")return t;switch(e._tag){case\"Filter\":return\"\";case\"FilterGroup\":return e.checks.map(r=>aI(r)).join(\" & \")}}function R4(){return e=>Vl(e,\"\")}var _P=R4();function Vl(e,t){let r;switch(e._tag){case\"Filter\":{let n=vP(e);if(n!==void 0)r=n;else{if(e.issue._tag!==\"InvalidValue\")return Vl(e.issue,t);let o=sI(e.issue);r=o===void 0?da(aI(e.filter),e):da(o,e.issue)}break}case\"Encoding\":return Vl(e.issue,t);case\"Pointer\":return Vl(e.issue,t+pd(e.path));case\"Composite\":case\"AnyOf\":{if(e._tag===\"Composite\"||e.issues.length>0)return e.issues.map(n=>Vl(n,t)).join(`\n`);r=uu(e)??da(Jl(e.ast),e);break}default:r=kP(e);break}return t?`${r}\n at ${t}`:r}function uu(e){if(e._tag===\"Pointer\")return;if(e._tag===\"Encoding\")return uu(e.issue);let r=(e._tag===\"Filter\"?e.filter.annotations:\"annotations\"in e?e.annotations:e.ast.annotations)?.[e._tag===\"MissingKey\"?\"messageMissingKey\":e._tag===\"UnexpectedKey\"?\"messageUnexpectedKey\":\"message\"];if(typeof r==\"string\")return r}function Xl(e){let t;for(let r of e.reasons){if(!na(r)||!nI(r.error))return;t??=r.error}return t}function ln(e,t){let r=Xl(e);if(r===void 0)throw new Error(t,{cause:e});return r}var PP=e=>su(e).replace(/=/g,\"\").replace(/\\+/g,\"-\").replace(/\\//g,\"_\"),iI=e=>{let t=e.replace(/[\\n\\r]/g,\"\"),r=t.length;if(r%4===1)return W(new An({module:\"Base64Url\",kind:\"Decode\",input:t,message:`Length should be a multiple of 4, but is ${r}`}));if(!/^[-_A-Z0-9]*?={0,2}$/i.test(t))return W(new An({module:\"Base64Url\",kind:\"Decode\",input:t,message:\"Invalid input\"}));let n=r%4===2?`${t}==`:r%4===3?`${t}=`:t;return n=n.replace(/-/g,\"+\").replace(/_/g,\"/\"),Ei(n)},MP=e=>Tr(iI(e),t=>P4.decode(t)),P4=new TextDecoder;var FP=(e,t,r)=>{switch(e._tag){case\"Passthrough\":return g(t);case\"Transform\":return g(Rt(t,e.transform));case\"TransformOptional\":return g(e.transform(t));case\"TransformEffect\":return fe(t)?aa:Dr(e.transform(t.value,r),O);case\"TransformOptionalEffect\":return e.transform(t,r)}};var ep=e=>Object.assign(Object.create(Lg),e),wh=(e,t)=>cI((r,n)=>st(FP(e,r,n),o=>FP(t,o,n))),Ch=l(2,(e,t)=>{if(e._tag===\"Passthrough\")return t;if(t._tag===\"Passthrough\")return e;switch(e._tag){case\"Transform\":switch(t._tag){case\"Transform\":return oe(r=>t.transform(e.transform(r)));case\"TransformOptional\":return Oi(r=>t.transform(Rt(r,e.transform)));case\"TransformEffect\":return gt((r,n)=>t.transform(e.transform(r),n));case\"TransformOptionalEffect\":return wh(e,t)}case\"TransformOptional\":switch(t._tag){case\"Transform\":return Oi(r=>Rt(e.transform(r),t.transform));case\"TransformOptional\":return Oi(r=>t.transform(e.transform(r)));case\"TransformEffect\":case\"TransformOptionalEffect\":return wh(e,t)}case\"TransformEffect\":switch(t._tag){case\"Transform\":return gt((r,n)=>Dr(e.transform(r,n),t.transform));case\"TransformOptional\":case\"TransformOptionalEffect\":return wh(e,t);case\"TransformEffect\":return gt((r,n)=>st(e.transform(r,n),o=>t.transform(o,n)))}case\"TransformOptionalEffect\":return wh(e,t)}});function F4(e){return cI((t,r)=>P(e(t,r)))}function Ah(e){return F4((t,r)=>{let n={message:e(t)};return be(t)?new Yl(n,t.value,r):new Yl(n)})}var NP=Ah(()=>\"Encoding is not supported\"),N4=ep({_tag:\"Passthrough\"});function jr(){return N4}function oe(e){return ep({_tag:\"Transform\",transform:e})}function gt(e){return ep({_tag:\"TransformEffect\",transform:e})}function Oi(e){return ep({_tag:\"TransformOptional\",transform:e})}function cI(e){return ep({_tag:\"TransformOptionalEffect\",transform:e})}function uI(){return Oi(()=>v())}function fI(e){return cI(t=>{let r=Va(t,Io);return be(r)?g(r):Dr(e,O)})}function _i(){return oe(globalThis.String)}function kh(){return oe(globalThis.Number)}function UP(){return oe(globalThis.BigInt)}function lI(){return oe(e=>new globalThis.Date(e))}function DP(){return oe(C_)}function LP(e){return gt((t,r)=>Qc({try:()=>JSON.parse(t,e?.reviver),catch:()=>new de({expected:\"a valid JSON string\"},t,r)}))}function $P(e){return gt((t,r)=>Qc({try:()=>{let n=JSON.stringify(t,e?.replacer,e?.space);if(n===void 0)throw new TypeError(\"Value cannot be represented as JSON\");return n},catch:()=>new de({expected:\"a JSON-serializable value\"},t,r)}))}function BP(e){let t=e?.separator??\",\",r=e?.keyValueSeparator??\"=\";return oe(n=>n.split(t).reduce((o,s)=>{if(r===\"\"){let[u,f]=s.split(\"\");return u&&f&&M(o,u,f),o}let a=s.indexOf(r);if(a<=0)return o;let i=s.slice(0,a),c=s.slice(a+r.length);return c&&M(o,i,c),o},{}))}function jP(e){let t=e?.separator??\",\",r=e?.keyValueSeparator??\"=\";return oe(n=>Object.entries(n).map(([o,s])=>`${o}${r}${s}`).join(t))}function zP(e){let t=e?.separator??\",\";return oe(r=>r===\"\"?[]:r.split(t))}function pI(){return oe(su)}function vh(){return oe(PP)}function Oh(){return oe(zA)}function qP(){return gt((e,t)=>Ul(Pl(Ei(e)),()=>new de({expected:\"a valid Base64 string\"},e,t)))}function HP(){return gt((e,t)=>wr(LR(e),{onFailure:()=>P(new de({expected:\"a valid Base64 string\"},e,t)),onSuccess:g}))}function WP(){return gt((e,t)=>wr(iI(e),{onFailure:()=>P(new de({expected:\"a valid Base64Url string\"},e,t)),onSuccess:g}))}function JP(){return gt((e,t)=>wr(MP(e),{onFailure:()=>P(new de({expected:\"a valid Base64Url string\"},e,t)),onSuccess:g}))}function KP(){return gt((e,t)=>wr(Oy(e),{onFailure:()=>P(new de({expected:\"a valid hexadecimal string\"},e,t)),onSuccess:g}))}function GP(){return gt((e,t)=>wr(qA(e),{onFailure:()=>P(new de({expected:\"a valid hexadecimal string\"},e,t)),onSuccess:g}))}function VP(){return oe(encodeURIComponent)}function ZP(){return gt((e,t)=>{try{return g(globalThis.decodeURIComponent(e))}catch{return P(new de({expected:\"a valid URI component\"},e,t))}})}function YP(){return oe(e=>tM(Array.from(e.entries())))}var U4=rM(e=>typeof e==\"string\"||typeof Blob<\"u\"&&e instanceof Blob);function QP(){return oe(e=>{let t=new FormData;return typeof e==\"object\"&&e!==null&&U4(e).forEach(([n,o])=>{t.append(n,o)}),t})}function XP(){return oe(e=>tM(Array.from(e.entries())))}var D4=rM(br);function eM(){return oe(e=>typeof e==\"object\"&&e!==null?new URLSearchParams(D4(e)):new URLSearchParams)}function L4(e){if(e===\"\")return[\"\"];let t=e.replace(/\\[(.*?)\\]/g,\".$1\"),r=t.split(\".\"),n=t.startsWith(\".\")?1:0;return r.slice(n).map(o=>Cc(o)?globalThis.Number(o):o)}function tM(e){let t={},r=new WeakSet,n=new WeakSet;function o(s,a,i){let c=Object.hasOwn(s,a)?s[a]:void 0;if(r.has(c)&&Array.isArray(c)===i)return c;let u=i?[]:{};return r.add(u),M(s,a,u),u}return e.forEach(([s,a])=>{let i=L4(s),c=t;i.forEach((u,f)=>{let p=f===i.length-1;if(Array.isArray(c)&&u===\"\")if(p)c.push(a);else{let d=i[f+1],m=typeof d==\"number\"||d===\"\",x=c.length;c=o(c,x,m)}else if(p){let d=Object.hasOwn(c,u);if(d&&Array.isArray(c[u])&&(r.has(c[u])||n.has(c[u])))c[u].push(a);else if(d){let m=[c[u],a];n.add(m),M(c,u,m)}else M(c,u,a)}else{let d=i[f+1];c=o(c,u,typeof d==\"number\"||d===\"\")}})}),t}function rM(e){return t=>{let r=[];function n(o,s){if(e(s))r.push([o,s]);else if(Array.isArray(s))s.every(e)?s.forEach(i=>{r.push([o,i])}):s.forEach((i,c)=>{n(`${o}[${c}]`,i)});else if(typeof s==\"object\"&&s!==null)for(let[a,i]of Object.entries(s))n(`${o}[${a}]`,i)}for(let[o,s]of Object.entries(t))n(o,s);return r}}var _h=class extends $s{_tag=\"Middleware\";decode;encode;constructor(e,t){super(),this.decode=e,this.encode=t}flip(){return new _h(this.encode,this.decode)}},tp=\"~effect/SchemaTransformation/Transformation\",ve=class extends $s{[tp]=tp;_tag=\"Transformation\";decode;encode;constructor(e,t){super(),this.decode=e,this.encode=t}flip(){return new ve(this.encode,this.decode)}};function $4(e){return k(e,tp)&&e[tp]===tp}var Rh=e=>$4(e)?e:new ve(e.decode,e.encode);function Yt(e){return new ve(gt(e.decode),gt(e.encode))}function Pe(e){return new ve(oe(e.decode),oe(e.encode))}function Ph(e){return new ve(Oi(e.decode),Oi(e.encode))}function nM(){return new ve(DP(),jr())}function oM(e){return new ve(BP(e),jP(e))}var B4=new ve(jr(),jr());function lu(){return B4}var pu=new ve(kh(),_i()),Mh=new ve(UP(),_i()),dI=new ve(lI(),oe(Yg)),sM=new ve(lI(),oe(e=>e.getTime()));var mI=Yt({decode:(e,t)=>Wt(hR(e),{onNone:()=>P(new de({expected:\"a valid ByteSize string\"},e,t)),onSome:g}),encode:e=>g(`${e} ${e===BigInt(1)?\"byte\":\"bytes\"}`)}),aM=Yt({decode:(e,t)=>Wt(lE(e),{onNone:()=>P(new de({expected:\"a non-negative bigint byte count\"},e,t)),onSome:g}),encode:e=>g(uh(e))}),iM=Yt({decode:(e,t)=>Wt(lE(e),{onNone:()=>P(new de({expected:\"a non-negative safe-integer byte count\"},e,t)),onSome:g}),encode:(e,t)=>Wt(yR(e),{onNone:()=>P(new de({expected:\"a ByteSize representable as a safe integer\"},e,t)),onSome:g})}),j4=e=>fr(e)&&typeof e.message==\"string\",cM=e=>{let r=Object.hasOwn(e,\"cause\")?new Error(e.message,{cause:fM(e.cause)}):new Error(e.message);return typeof e.name==\"string\"&&e.name!==\"Error\"&&(r.name=e.name),typeof e.stack==\"string\"&&(r.stack=e.stack),r},z4=e=>{try{let t=Ua(e);return t===void 0?F(e):JSON.parse(t)}catch{return F(e)}},q4=(e,t,r)=>{let n={name:e.name,message:typeof e.message==\"string\"?e.message:\"\"};return t?.includeStack&&typeof e.stack==\"string\"&&(n.stack=e.stack),!t?.excludeCause&&e.cause!==void 0&&(n.cause=r(e.cause)),n},uM=e=>{let t=new WeakSet,r=n=>{if(cC(n)){if(t.has(n))return\"[Circular]\";t.add(n);let o=q4(n,e,r);return t.delete(n),o}return z4(n)};return r},fM=e=>j4(e)?cM(e):e,lM=e=>Pe({decode:cM,encode:t=>uM(e)(t)}),pM=e=>Pe({decode:fM,encode:uM(e)});function dM(){return Pe({decode:cy,encode:Rd})}function mM(){return Pe({decode:iy,encode:qs})}function hM(e){return Pe({decode:Nf,encode:e?.onNoneEncoding===null?Rd:qs})}function xM(){return Ph({decode:O,encode:Ga})}function gM(){return Ph({decode:e=>e.pipe(Va(Io),O),encode:Ga})}var hI=Yt({decode:(e,t)=>URL.canParse(e)?g(new URL(e)):P(new de({expected:\"a valid URL string\"},e,t)),encode:e=>g(e.href)});var xI=new ve(qP(),pI()),yM=new ve(HP(),pI()),SM=new ve(JP(),vh()),bM=new ve(GP(),Oh()),EM=new ve(ZP(),VP());function IM(e){return new ve(LP(e??{}),$P(e))}var TM=new ve(YP(),QP()),wM=new ve(XP(),eM());function xa(e){return t=>t._tag===e}var gu=xa(\"Declaration\");var H4=xa(\"Never\");var mu=xa(\"Literal\"),yI=xa(\"UniqueSymbol\");var AI=xa(\"Arrays\"),W4=xa(\"Objects\"),MM=xa(\"Union\"),FM=xa(\"Suspend\"),at=class{to;transformation;constructor(e,t){this.to=e,this.transformation=t}},Go={},Ko=class{isOptional;isMutable;constructorDefault;annotations;constructor(e,t,r=void 0,n=void 0){this.isOptional=e,this.isMutable=t,this.constructorDefault=r,this.annotations=n}},CM=\"~effect/Schema\",lt=class{[CM]=CM;annotations;checks;encoding;context;constructor(t=void 0,r=void 0,n=void 0,o=void 0){this.annotations=t,this.checks=r,this.encoding=n,this.context=o}toString(){return`<${this._tag}>`}},yu=class extends lt{_tag=\"Declaration\";typeParameters;run;encodingChecks;encodingRun;constructor(e,t,r,n,o,s,a,i){super(r,n,o,s),this.typeParameters=e,this.run=t,this.encodingChecks=a,this.encodingRun=i}getParser(){let e;return(t,r)=>t===ge?$r:(e??=this.run(this.typeParameters))(t,this,r)}_rebuild(e,t,r,n,o){let s=Pi(this.typeParameters,e);return s===this.typeParameters&&t===this.checks&&r===this.encodingChecks&&n===this.run&&o===this.encodingRun?this:new yu(s,n,this.annotations,t,void 0,this.context,r,o)}recur(e){return this._rebuild(e,this.checks,this.encodingChecks,this.run,this.encodingRun)}flip(e){return this._rebuild(e,this.encodingChecks,this.checks,this.encodingRun??this.run,this.run)}getExpected(){let e=this.annotations?.expected;return typeof e==\"string\"?e:\"\"}},J4=class extends lt{_tag=\"Null\";getParser(){return qh(this,null)}getExpected(){return\"null\"}},kI=new J4;var K4=class extends lt{_tag=\"Undefined\";getParser(){return qh(this,void 0)}toCodecJson(){return We(this,[NM])}getExpected(){return\"undefined\"}},NM=new at(kI,new ve(oe(()=>{}),oe(()=>null))),vI=new K4;var G4=class extends lt{_tag=\"Void\";getParser(){let e=Zt(void 0);return t=>t===ge?$r:e}toCodecJson(){return We(this,[NM])}getExpected(){return\"void\"}},UM=new G4;var V4=class extends lt{_tag=\"Never\";getParser(){return ro(this,iC)}getExpected(){return\"never\"}},OI=new V4,Z4=class extends lt{_tag=\"Any\";getParser(){return ro(this,zg)}getExpected(){return\"any\"}},DM=new Z4,Y4=class extends lt{_tag=\"Unknown\";getParser(){return ro(this,zg)}getExpected(){return\"unknown\"}},eo=new Y4,Q4=class extends lt{_tag=\"ObjectKeyword\";getParser(){return ro(this,If)}getExpected(){return\"object | array | function\"}},LM=new Q4,$M=class extends lt{_tag=\"Enum\";enums;constructor(e,t,r,n,o){super(t,r,n,o);for(let[,s]of e)if(typeof s==\"number\"&&!globalThis.Number.isFinite(s))throw new Error(`A numeric enum value must be finite, got ${F(s)}`);this.enums=e}getParser(){let e=new Set(this.enums.map(([,t])=>t));return ro(this,t=>e.has(t))}toCodecStringTree(){if(this.enums.some(([e,t])=>typeof t==\"number\")){let e=Object.fromEntries(this.enums.map(([t,r])=>[globalThis.String(r),r]));return We(this,[new at(new zr(Object.keys(e).map(t=>new dn(t))),new ve(oe(t=>e[t]),_i()))])}return this}getExpected(){return this.enums.map(([e,t])=>JSON.stringify(t)).join(\" | \")}};function gI(e){return typeof e==\"number\"?`parts[${e}]`:e}function SI(e,t,r){if(r.has(e))return!0;if(e.encoding)throw new Error(`TemplateLiteral parts cannot have an encoding at ${gI(t)}`);let n;switch(e._tag){case\"String\":case\"Number\":case\"BigInt\":n=!0;break;case\"Literal\":n=!e.checks;break;case\"TemplateLiteral\":{let o=gI(t);n=!e.checks&&e.parts.every((s,a)=>SI(s,`${o}.parts[${a}]`,r));break}case\"Union\":{let o=gI(t);n=!e.checks&&e.types.every((s,a)=>SI(s,`${o}.types[${a}]`,r));break}default:return!1}return n&&r.add(e),n}var _I=class extends lt{_tag=\"TemplateLiteral\";parts;encodedParts;literals;suffixLengths;constructor(e,t,r,n,o){super(t,r,n,o);let s=[],a=[],i=new Set;for(let u=0;u=0;u--)c[u]=c[u+1]+(a[u]?.length??0);this.parts=e,this.encodedParts=s,this.literals=a,this.suffixLengths=c}getParser(e){let t=new Pn(!1,this.parts.map(WI),[]),r=e(ga(pn,t,jM(this)));return(n,o)=>{if(n===ge)return $r;let s=r(n,o);return s._tag===\"Success\"?qe:Vt(s,{onSuccess:()=>n,onFailure:a=>new wt(this,[a],n,o)})}}getExpected(){return\"string\"}matchPart(e,t){return dF(this,e,t)===void 0?void 0:e}};function BM(e){let t=Vr(o=>{if(o._tag!==\"Union\")return o;let s=Pi(o.types,t);return(o.options?.mode??\"anyOf\")===\"anyOf\"&&s===o.types?o:new zr(s,o.options===void 0?void 0:{...o.options,mode:\"anyOf\"},o.annotations,o.checks,void 0,o.context)}),r=new _I(e.map(o=>t(hn(o)))),n=new Pn(!1,e.map(WI),[]);return ga(r,n,jM(r))}function jM(e){return new ve(gt((t,r)=>{let n=dF(e,t,r);return n?g(n):P(new de({expected:\"a string matching template literal parts\"},t,r))}),oe(t=>t.join(\"\")))}var zM=class extends lt{_tag=\"UniqueSymbol\";symbol;constructor(e,t,r,n,o){super(t,r,n,o),this.symbol=e}getParser(){return qh(this,this.symbol)}toCodecStringTree(){let e=globalThis.Symbol.keyFor(this.symbol);return We(this,[new at(e===void 0?OI:new dn(globalThis.String(this.symbol)),xF.transformation)])}getExpected(){return globalThis.String(this.symbol)}},dn=class extends lt{_tag=\"Literal\";literal;constructor(e,t,r,n,o){if(super(t,r,n,o),typeof e==\"number\"&&!globalThis.Number.isFinite(e))throw new Error(`A numeric literal must be finite, got ${F(e)}`);this.literal=e}getParser(){return qh(this,this.literal)}matchPart(e,t){return e===globalThis.String(this.literal)?this.literal:void 0}toCodecJson(){return typeof this.literal==\"bigint\"?AM(this):this}toCodecStringTree(){return typeof this.literal==\"string\"?this:AM(this)}getExpected(){return typeof this.literal==\"string\"?JSON.stringify(this.literal):globalThis.String(this.literal)}};function AM(e){let t=globalThis.String(e.literal);return We(e,[new at(new dn(t),new ve(oe(()=>e.literal),oe(()=>t)))])}var X4=class extends lt{_tag=\"String\";getParser(){return ro(this,br)}matchPart(e,t){let r=this.checks;return r&&!t.disableChecks&&no(r,e,void 0,this,t)?void 0:e}getExpected(){return\"string\"}},pn=new X4,e6=class extends lt{_tag=\"Number\";getParser(){return ro(this,Ef)}matchKey(e,t){return this._match(y6,e,t)}matchPart(e,t){return this._match(TI,e,t)}_match(e,t,r){if(!e.test(t))return;let n=globalThis.Number(t);return r.disableChecks||!this.checks?n:no(this.checks,n,void 0,this,r)?void 0:n}toCodecJson(){return this.checks&&(bI(this.checks,\"effect/schema/isFinite\")||bI(this.checks,\"effect/schema/isInt\"))?this:We(this,[l6])}toCodecStringTree(){return this.toCodecJson()===this?We(this,[S6]):We(this,[b6])}getExpected(){return\"number\"}};function bI(e,t){return e.some(r=>r.annotations?.representation?.id===t||r._tag===\"FilterGroup\"&&bI(r.checks,t))}var RI=new e6,t6=class extends lt{_tag=\"Boolean\";getParser(){return ro(this,ad)}getExpected(){return\"boolean\"}},qM=new t6,r6=class extends lt{_tag=\"Symbol\";getParser(){return ro(this,jg)}matchKey(e,t){return t.disableChecks||!this.checks?e:no(this.checks,e,void 0,this,t)?void 0:e}toCodecStringTree(){return We(this,[xF])}getExpected(){return\"symbol\"}},HM=new r6,n6=class extends lt{_tag=\"BigInt\";getParser(){return ro(this,sC)}matchPart(e,t){if(!wI.test(e))return;let r=globalThis.BigInt(e);return t.disableChecks||!this.checks?r:no(this.checks,r,void 0,this,t)?void 0:r}toCodecStringTree(){return We(this,[I6])}getExpected(){return\"bigint\"}},WM=new n6,Pn=class extends lt{_tag=\"Arrays\";isMutable;elements;rest;encodingChecks;constructor(e,t,r,n,o,s,a,i){super(n,o,s,a),this.isMutable=e,this.elements=t,this.rest=r,this.encodingChecks=i;let c=!1;for(let u=0;u1)throw new Error(\"A required element cannot follow an optional element. ts(1257)\");for(let u=1;u=f?o[p-f+1]:o[0]}let c=f=>{let{input:p,len:d,options:m}=f;if(r.rest.length===0&&d>s)for(let x=s;x{let d=f.length,m={ast:r,getParser:i,input:f,len:d,tailThreshold:Math.max(s,d-a),output:new globalThis.Array(d),issues:void 0,options:p},x=r.rest.length===0?s:Math.max(d,s+a),y=p.concurrency===void 0?1:pl(p.concurrency),S=y===1?s6(m,f,0,x):a6(m,f,{concurrency:y,end:x});if(!S)return c(m);if(Ne(S))return st(S,()=>c(m));let w=!0;return $(()=>w?(w=!1,I(S,()=>c(m))):u(f,p))};return(f,p)=>{if(f===ge)return $r;try{return Array.isArray(f)?(n||(n=r.elements.map(d=>({ast:d,parser:t(d)})),o=r.rest.map(d=>({ast:d,parser:t(d)}))),u(f,p)):P(new ft(r,f,p))}catch(d){return an(d)}}}_rebuild(e,t,r){let n=Pi(this.elements,e),o=Pi(this.rest,e);return n===this.elements&&o===this.rest&&t===this.checks&&r===this.encodingChecks?this:new Pn(this.isMutable,n,o,this.annotations,t,void 0,this.context,r)}recur(e){return this._rebuild(e,this.checks,this.encodingChecks)}flip(e){return this._rebuild(e,this.encodingChecks,this.checks)}getExpected(){return\"array\"}};function o6(e,t,r,n){if(r._tag===\"Failure\")return Nh(e,e.ast,n,r);let o=r===qe?t:r[ue];if(o!==ge)e.output[n]=o;else{let s=e.getParser(e.tailThreshold,n);if(Qt(s.ast))return;let a=new ut([n],new ma(s.ast.context?.annotations));if(e.options.errors===\"all\")e.issues?e.issues.push(a):e.issues=[a];else return bs(new wt(e.ast,[a],e.input,e.options))}}var JM={onItem(e,t,r){let n=r{if(n.cause.reasons.length===0)return n;let o=Xl(n.cause);if(o===void 0)return sn(Ss(n.cause,a=>new wt(t,[new ut([r],a)],e.input,e.options)));let s=new ut([r],o);if(e.options.errors===\"all\")e.issues?e.issues.push(s):e.issues=[s];else return bs(new wt(t,[s],e.input,e.options))},Dh=\"[+-]?\\\\d*\\\\.?\\\\d+(?:[Ee][+-]?\\\\d+)?\";function ha(e,t,r=Go){let n,o;function s(a){switch(a._tag){case\"String\":case\"TemplateLiteral\":return(n??=Object.keys(e)).filter(i=>a.matchPart(i,r)!==void 0);case\"Number\":return(n??=Object.keys(e)).filter(i=>a.matchKey(i,r)!==void 0);case\"Symbol\":return(o??=Object.getOwnPropertySymbols(e)).filter(i=>Object.prototype.propertyIsEnumerable.call(e,i)&&a.matchKey(i,r)!==void 0);case\"Union\":return[...new Set(a.types.flatMap(s))];default:return[]}}return s(HI(hn(t)))}var PI=class{name;type;constructor(e,t){this.name=e,this.type=t}};function KM(e){switch(e._tag){case\"String\":case\"Number\":case\"Symbol\":case\"TemplateLiteral\":return!0;case\"Union\":return e.types.every(KM);default:return!1}}function GM(e){let t=bu(e);switch(t._tag){case\"String\":case\"Number\":case\"Symbol\":case\"TemplateLiteral\":return!0;case\"Union\":return t.types.every(GM);default:return!1}}function i6(e){return KM(e)&&GM(e)}var MI=class{parameter;type;constructor(e,t){if(!i6(e))throw new Error(`Invalid index signature parameter ${e._tag}`);if(this.parameter=e,this.type=t,Qt(t)&&!pF(t))throw new Error(\"Cannot use `Schema.optionalKey` with index signatures, use `Schema.optional` instead.\")}},np=class extends lt{_tag=\"Objects\";propertySignatures;indexSignatures;encodingChecks;constructor(e,t,r,n,o,s,a){super(r,n,o,s),this.propertySignatures=e,this.indexSignatures=t,this.encodingChecks=a}getParser(e,t=e){let r=this,n=r.propertySignatures.length,o=r.indexSignatures.length;if(!n&&!o)return ro(r,cd);let s,a,i=()=>(s||(s=r.propertySignatures.map(p=>({parser:t(p.type),name:p.name,type:p.type})),a=o?r.indexSignatures.map(p=>({is:p,parserKey:e(HI(p.parameter)),parserValue:t(p.type)})):void 0),s),c=()=>{let p=new Set(r.propertySignatures.map(S=>typeof S.name==\"number\"?globalThis.String(S.name):S.name)),d=(S,w,E,U,_)=>{if(_._tag===\"Failure\")return Nh(S,r,w,_)??xt;let h=_===qe?U:_[ue];if(E!==ge&&h!==ge){if(n&&(p.has(w)||p.has(typeof E==\"number\"?globalThis.String(E):E)))return xt;M(S.out,E,h)}return xt},m=(S,w,E,U)=>{if(!U){let T=E.parserKey(w,S.options);if(!Ne(T))return I(vn(T),C=>m(S,w,E,C));U=T}if(U._tag===\"Failure\")return Nh(S,r,w,U)??xt;let _=U===qe?w:U[ue],h=S.input[w],b=E.parserValue(h,S.options);return Ne(b)?d(S,w,_,h,b):I(vn(b),T=>d(S,w,_,h,T))},x=(S,w,E)=>{let U=S.input[w],_=E.parserValue(U,S.options);return Ne(_)?d(S,w,w,U,_):I(vn(_),h=>d(S,w,w,U,h))},y=o?vm()({onItem:(S,[w,E])=>E.is.parameter===pn?x(S,w,E):m(S,w,E),step:(S,w,E)=>E._tag===\"Failure\"?E:void 0}):void 0;return oE(function*(S,w){if(S===ge)return ge;if(!(typeof S==\"object\"&&S!==null&&!Array.isArray(S)))return yield*P(new ft(r,S,w));i();let E=S,U={},_={ast:r,input:E,out:U,issues:void 0,options:w},h=w.errors===\"all\",b=w.onExcessProperty===\"error\",T=w.concurrency===void 0?1:pl(w.concurrency),C=o&&b?r.indexSignatures.map(L=>ha(E,L.parameter,w)):void 0;if(b){let L=C?new Set(p):p;if(C)for(let ye of C)for(let _e of ye)L.add(_e);let ee=Reflect.ownKeys(E);for(let ye=0;ye{let x=s[d];return I(vn(m),y=>{let S=EI(p,x,y);if(S)return S;let w=()=>Zt(p.out),E=kM(p,s.slice(d+1));return E?st(E,w):w()})};return(p,d)=>{if(p===ge)return $r;if(d.errors===\"all\"||d.onExcessProperty!==void 0||d.concurrency!==void 0&&pl(d.concurrency)!==1)return(u??=c())(p,d);if(!(typeof p==\"object\"&&p!==null&&!Array.isArray(p)))return P(new ft(r,p,d));let m=i(),x=p,y={},S={ast:r,input:x,out:y,issues:void 0,options:d};try{for(let w=0;w{let i=e(a.type);return i===a.type?a:new PI(a.name,i)}),s=Pi(this.indexSignatures,a=>{let i=t(a.parameter),c=e(a.type);return i===a.parameter&&c===a.type?a:new MI(i,c)});return o===this.propertySignatures&&s===this.indexSignatures&&r===this.checks&&n===this.encodingChecks?this:new np(o,s,this.annotations,r,void 0,this.context,n)}flip(e){return this._rebuild(e,e,this.encodingChecks,this.checks)}recur(e,t=e){return this._rebuild(e,t,this.checks,this.encodingChecks)}getExpected(){return this.propertySignatures.length===0&&this.indexSignatures.length===0?\"object | array\":\"object\"}};function EI(e,t,r){if(r._tag===\"Failure\")return Nh(e,e.ast,t.name,r);if(r===qe)return;let n=r[ue];if(n!==ge){M(e.out,t.name,n);return}if(delete e.out[t.name],!Qt(t.type)){let o=new ut([t.name],new ma(t.type.context?.annotations));if(e.options.errors===\"all\"){e.issues?e.issues.push(o):e.issues=[o];return}else return bs(new wt(e.ast,[o],e.input,e.options))}}var VM={onItem(e,t){if(!du(e.input,t.name))return t.parser(ge,e.options);let r=e.input[t.name];return M(e.out,t.name,r),t.parser(r,e.options)},step:EI},kM=km()(VM),c6=vm()(VM);function FI(e,t){return e?t?[...e,...t]:e:t}function Lh(e,t,r){return new np(Reflect.ownKeys(e).map(n=>new PI(n,e[n].ast)),[],r,t)}function op(e){return e.ast}function NI(e,t=void 0){return new Pn(!1,e.map(r=>r.ast),[],void 0,t)}function $h(e,t,r){return new zr(e.map(op),t,void 0,r)}function ZM(e,t){if(e.encoding||t.some(s=>s.encoding))throw new Error(\"StructWithRest does not support encodings\");let r=e.propertySignatures,n=e.indexSignatures,o=e.checks;for(let s of t){for(let a of s.propertySignatures)if(r.some(i=>i.name===a.name))throw new Error(`Duplicate identifier: ${JSON.stringify(a.name)}. ts(2300)`);r=r.concat(s.propertySignatures),n=n.concat(s.indexSignatures),o=FI(o,s.checks)}return new np(r,n,void 0,o)}function YM(e,t){if(e.encoding)throw new Error(\"TupleWithRest does not support encodings\");return new Pn(e.isMutable,e.elements,t,void 0,e.checks)}function QM(e){if(e.encoding)throw new Error(\"mutable does not support encodings\");return new Pn(!0,e.elements,e.rest,e.annotations,e.checks,void 0,e.context,e.encodingChecks)}var UI=Pa(e=>{for(;;){if(FM(e))return eo;let t=e.encoding;if(!t)return e.recur?.(UI,R)??e;if(t.some(r=>r.transformation._tag===\"Middleware\"&&r.transformation.decode!==R))return eo;e=t[t.length-1].to}});function XM(e){switch(e._tag){case\"Null\":return[\"null\"];case\"Undefined\":return[\"undefined\"];case\"String\":case\"TemplateLiteral\":return[\"string\"];case\"Number\":return[\"number\"];case\"Boolean\":return[\"boolean\"];case\"Symbol\":case\"UniqueSymbol\":return[\"symbol\"];case\"BigInt\":return[\"bigint\"];case\"Arrays\":return[\"array\"];case\"ObjectKeyword\":return[\"object\",\"array\",\"function\"];case\"Objects\":return e.propertySignatures.length||e.indexSignatures.length?[\"object\"]:[\"string\",\"number\",\"boolean\",\"symbol\",\"bigint\",\"object\",\"array\",\"function\"];case\"Enum\":return Array.from(new Set(e.enums.map(([,t])=>typeof t)));case\"Literal\":return[typeof e.literal];case\"Union\":return Array.from(new Set(e.types.flatMap(XM)));default:return[\"null\",\"undefined\",\"string\",\"number\",\"boolean\",\"symbol\",\"bigint\",\"object\",\"array\",\"function\"]}}function hu(e){switch(e._tag){default:return[];case\"Declaration\":{let t=e.annotations?.[Wl];return Array.isArray(t)?t:[]}case\"Objects\":return e.propertySignatures.flatMap(t=>{let r=t.type;if(!Qt(r)){if(mu(r))return[{key:t.name,literal:r.literal}];if(yI(r))return[{key:t.name,literal:r.symbol}]}return[]});case\"Arrays\":return e.elements.flatMap((t,r)=>{if(!Qt(t)){if(mu(t))return[{key:r,literal:t.literal}];if(yI(t))return[{key:r,literal:t.symbol}]}return[]});case\"Union\":{if(e.types.length===0)return[];let t=e.types.map(r=>hu(UI(r)));return t[0].filter(r=>t.every(n=>n.some(o=>o.key===r.key&&o.literal===r.literal)))}case\"Suspend\":return hu(e.thunk())}}var vM=new WeakMap,Fh=Object.freeze([]),OM=e=>e===null?\"null\":Array.isArray(e)?\"array\":typeof e,du=(e,t)=>t===\"__proto__\"?Object.hasOwn(e,t):t in e;function sp(e){let t=vM.get(e);if(t)return t;let r,n=0,o,s,a=!0,i=[];for(let f=0;fc[f]??=Object.freeze(o?.[f]??Fh);if(a&&s)s.forEach(Object.freeze),t=f=>s.get(f)??Fh;else if(r?.size===1&&!o){let[f,[p]]=r.entries().next().value,d=new Map;for(let[x,y]of p)d.set(x,Object.freeze(Array.from(y)));let m=Object.freeze(e.map((x,y)=>y));t=(x,y)=>{if(If(x)){let S=du(x,f)?x[f]:void 0;if(S!==void 0)return d.get(S)??Fh;if(y)return m}return Fh}}else if(r){let f;for(let p of r)(!f||p[1][0].size>f[1][0].size)&&p[1][1].size===n&&(f=p);t=(p,d)=>{let m=OM(p);if(!If(p))return u(m);let x=new Set(o?.[m]),y;if(f){let[S,[w]]=f,E=du(p,S),U=E?p[S]:void 0;if(E&&(!d||U!==void 0)){let _=w.get(U);if(!_)return u(m);for(let h of _)x.add(h);y=S}}if(y===void 0)for(let[S,[w,E]]of r){let U=du(p,S),_=U?p[S]:void 0;if(U&&(!d||_!==void 0)){let h=w.get(_);if(h)for(let b of h)x.add(b)}else if(d)for(let h of E)x.add(h)}for(let[S,[w,E]]of r){if(S===y)continue;let U=du(p,S),_=U?p[S]:void 0;if(U&&(!d||_!==void 0)){let h=w.get(_);for(let b of x)E.has(b)&&!h?.has(b)&&x.delete(b)}}return Array.from(x).sort((S,w)=>S-w)}}else t=f=>{let p=u(OM(f));return s?p.filter(d=>i[d]===void 0||i[d]===f):p};return vM.set(e,t),t}var zr=class extends lt{_tag=\"Union\";types;options;encodingChecks;constructor(e,t,r,n,o,s,a){super(r,n,o,s),this.types=e,this.options=t,this.encodingChecks=a}getParser(e,t){let r=this,n=t!==void 0,o=[],s=i=>o[i]??=e(r.types[i]),a;return(i,c)=>{if(i===ge)return $r;let u=(a??=sp(r.types))(i,n);if(u.length===0)return P(new Ql(r,[],i,c));if(u.length===1){let f=s(u[0])(i,c);return f._tag===\"Success\"?f:Ne(f)?eF(r,f.cause,i,c):u6(r,f,i,c)}return tF(r,s,u,i,c)}}_rebuild(e,t,r){let n=Pi(this.types,e);return n===this.types&&t===this.checks&&r===this.encodingChecks?this:new zr(n,this.options,this.annotations,t,void 0,this.context,r)}recur(e){return this._rebuild(e,this.checks,this.encodingChecks)}flip(e){return this._rebuild(e,this.encodingChecks,this.checks)}matchPart(e,t){for(let r of this.types){let n=r.matchPart(e,t);if(n!==void 0)return n}}getExpected(e){let t=this.annotations?.expected;if(typeof t==\"string\")return t;if(this.types.length===0)return\"never\";let r=this.types.map(n=>{let o=hn(n);switch(o._tag){case\"Arrays\":{let s=o.elements.filter(mu);if(s.length>0)return`${RM(o.isMutable)}[ ${s.map(a=>e(a)+PM(a.context?.isOptional)).join(\", \")}, ... ]`;break}case\"Objects\":{let s=o.propertySignatures.filter(a=>mu(a.type));if(s.length>0)return`{ ${s.map(a=>`${RM(a.type.context?.isMutable)}${wo(a.name)}${PM(a.type.context?.isOptional)}: ${e(a.type)}`).join(\", \")}, ... }`;break}}return e(o)});return Array.from(new Set(r)).join(\" | \")}};function eF(e,t,r,n){let o=Xl(t);return o?bs(new Ql(e,[o],r,n)):sn(t)}function u6(e,t,r,n){return rt(t,o=>eF(e,o,r,n))}function tF(e,t,r,n,o){let s={ast:e,parser:t,input:n,out:void 0,successes:e.options?.mode===\"oneOf\"?[]:void 0,issues:void 0,options:o},a=f6(s,r);if(!a)return s.out?s.out:P(new Ql(e,s.issues??[],n,o));if(Ne(a))return _M(a,s);let i=!0;return $(()=>i?(i=!1,_M(a,s)):tF(e,t,r,n,o))}function _M(e,t){return st(e,r=>t.out===qe?g(t.input):t.out?t.out:P(new Ql(t.ast,t.issues??[],t.input,t.options)))}var f6=km()({onItem(e,t){return e.parser(t)(e.input,e.options)},step(e,t,r){if(r._tag===\"Failure\"){let n=Xl(r.cause);if(n===void 0)return r;e.issues?e.issues.push(n):e.issues=[n]}else{if(e.out&&e.successes)return e.successes.push(e.ast.types[t]),bs(new wP(e.ast,e.successes,e.input,e.options));if(e.out=r,e.successes)e.successes.push(e.ast.types[t]);else return xt}}}),rF=new zr([new dn(\"Infinity\"),new dn(\"-Infinity\"),new dn(\"NaN\")]);function RM(e){return e?\"\":\"readonly \"}function PM(e){return e?\"?\":\"\"}var DI=class extends lt{_tag=\"Suspend\";thunk;constructor(e,t,r,n,o){if(r)throw new Error(\"Cannot add checks to Suspend\");super(t,void 0,n,o);let s;this.thunk=()=>s??=e()}getParser(e){let t;return(r,n)=>(t??=e(this.thunk()))(r,n)}recur(e){return new DI(()=>e(this.thunk()),this.annotations,void 0,void 0,this.context)}getExpected(e){return e(this.thunk())}},Uh=class extends $s{_tag=\"Filter\";run;annotations;aborted;constructor(e,t=void 0,r=!1){super(),this.run=e,this.annotations=t,this.aborted=r}annotate(e){return new Uh(this.run,{...this.annotations,...e},this.aborted)}abort(){return new Uh(this.run,this.annotations,!0)}and(e,t){return new Ri([this,e],t)}},Ri=class extends $s{_tag=\"FilterGroup\";checks;annotations;constructor(e,t=void 0){super(),this.checks=e,this.annotations=t}annotate(e){return new Ri(this.checks,{...this.annotations,...e})}and(e,t){return new Ri([this,e],t)}};function Bh(e,t,r=!1){return new Uh((n,o,s)=>AP(o,e(n,o,s),n,s),t,r)}function jh(e,t){return new Uh((r,n,o)=>e(r)?void 0:new de(void 0,r,o),t,!0)}function LI(e){return Bh(t=>globalThis.Number.isFinite(t),{expected:\"a finite number\",representation:{id:\"effect/schema/isFinite\",payload:null},toJsonSchema:()=>({type:\"number\"}),toCode:()=>({runtime:\"Schema.isFinite()\"}),arbitraryConstraint:{number:\"finite\"},...e})}var $I=Vo(RI,[LI()]),l6=new at(new zr([$I,rF]),new ve(kh(),oe(e=>globalThis.Number.isFinite(e)?e:globalThis.String(e))));function ap(e,t){let r=new globalThis.RegExp(e),n={source:r.source,flags:r.flags};return Bh(o=>(r.lastIndex=0,r.test(o)),{expected:`a string matching the RegExp ${n.source}`,representation:{id:\"effect/schema/isPattern\",payload:n},toJsonSchema:()=>[{},!0],arbitraryConstraint:{patterns:[n]},...t})}var II=new WeakMap;function ip(e,t){let r=Object.assign(Object.create(Object.getPrototypeOf(e)),e,t);return Reflect.ownKeys(t).every(n=>n===\"context\"||n===\"encoding\")&&II.set(r,Su(e)),r}function Su(e){let t=II.get(e);if(t!==void 0)return t;if(e.encoding===void 0)return e;let r=Object.assign(Object.create(Object.getPrototypeOf(e)),e,{encoding:void 0});return II.set(e,r),r}function We(e,t){return e.encoding===t?e:ip(e,{encoding:t})}function Mi(e,t){if(e.context===t)return e;let r=Su(e);return r.context===t&&r.encoding===e.encoding?r:ip(e,{context:t})}function bu(e){return e.encoding?bu(e.encoding[e.encoding.length-1].to):e}function to(e,t){if(e.checks){let r=e.checks[e.checks.length-1];return nF(e,$d(e.checks.slice(0,-1),r.annotate(t)))}return ip(e,{annotations:{...e.annotations,...t}})}function nF(e,t){if(e._tag===\"Suspend\"&&t)throw new Error(\"Cannot add checks to Suspend\");return e.checks===t?e:ip(e,{checks:t})}function Vo(e,t){return nF(e,FI(e.checks,t))}function Eu(e,t){let r=t(e.to);return r===e.to?e:new at(r,e.transformation)}function BI(e,t){let r=e,n=r[r.length-1],o=Eu(n,t);return o===n?e:$d(e.slice(0,e.length-1),o)}function jI(e){return t=>t.encoding?We(t,BI(t.encoding,e)):t}function zh(e,t){return jI(r=>Mi(r,t))(e)}function oF(e){function t(r){return r.encoding?We(r,BI(r.encoding,t)):e(r)}return Vr(t)}function Fi(e,t){function r(n){if(n.encoding){let o=n.encoding[n.encoding.length-1];return t?.stopAt?.(o)?n:We(n,BI(n.encoding,r))}return e(n)}return Pa(r)}function sF(e,t){return zI(e,t,yt(e))}function aF(e,t){return zI(hn(e),t,e)}function zI(e,t,r){let n=new at(e,t);return We(r,r.encoding?[...r.encoding,n]:[n])}function Pi(e,t){let r;for(let n=0;n{let t=e.context?e.context.isOptional===!1?new Ko(!0,e.context.isMutable,e.context.constructorDefault,e.context.annotations):e.context:new Ko(!0,!1);return p6(Mi(e,t))}),p6=jI(Ni),iF=Vr(e=>Ni(new zr([e,vI]))),qI=Pa(e=>{let t=e.context?e.context.isMutable===!1?new Ko(e.context.isOptional,!0,e.context.constructorDefault,e.context.annotations):e.context:new Ko(!1,!0);return d6(Mi(e,t))}),d6=jI(qI);function cF(e,t){let r=e.context?new Ko(e.context.isOptional,e.context.isMutable,t,e.context.annotations):new Ko(!1,!1,t);return Mi(e,r)}function ga(e,t,r){return zI(e,r,t)}function m6(e){let t=[],r=[];function n(o){switch(o._tag){case\"Literal\":id(o.literal)&&!t.includes(o.literal)&&t.push(o.literal);return;case\"UniqueSymbol\":t.includes(o.symbol)||t.push(o.symbol);return;case\"Never\":return;case\"Union\":for(let s=0;snew PI(o,t)),n.map(o=>new MI(o,t)))}function Qt(e){return e.context?.isOptional??!1}function fF(e){return e.context?.isMutable??!1}function lF(e){return e.annotations?.[Lr]===!0||e._tag===\"FilterGroup\"&&e.checks.every(lF)}function h6(e){function t(n){return lF(n)?[n]:n._tag===\"FilterGroup\"?n.checks.flatMap(t):[]}let r=e.flatMap(t);return or(r)?r:void 0}function x6(e){let t=!0;function r(n){return t=t&&!n.encoding&&!FM(n),t&&\"recur\"in n&&n.recur(r),n}return\"recur\"in e&&e.recur(r),t}var yt=Pa(e=>{let t=Su(e);if(t!==e){let n=yt(t);return n===t&&e.encoding===void 0?e:Mi(n,e.context)}let r=\"recur\"in e?e.recur(yt):e;if(\"encodingChecks\"in r&&r.encodingChecks){let n=x6(e)?r.encodingChecks:AI(r)||W4(r)||gu(r)&&r.typeParameters.length>0?h6(r.encodingChecks):void 0;return ip(r,{encodingChecks:void 0,checks:FI(r.checks,n)})}return r}),hn=Pa(e=>yt(mn(e)));function g6(e,t){let r=t,n=r.length,o=r[n-1],s=[new at(mn(We(e,void 0)),r[0].transformation.flip())];for(let i=1;i{if(e.encoding)return g6(e,e.encoding);let t=Su(e);if(t!==e){let r=mn(t);return r===t?e:Mi(r,e.context)}return\"flip\"in e?e.flip(mn):\"recur\"in e?e.recur(mn):e});function pF(e){switch(e._tag){case\"Undefined\":return!0;case\"Union\":return e.types.some(pF);default:return!1}}function qh(e,t){let r=t===0?qe:Zt(t);return(n,o)=>n===ge?$r:n===t?r:P(new ft(e,n,o))}function ro(e,t){return(r,n)=>r===ge?$r:t(r)?qe:P(new ft(e,r,n))}function dF(e,t,r){let n=e.encodedParts,o=e.literals,s=t.length;for(let u=0;us)return;let a=new Array(n.length),i;function c(u,f){if(u===n.length)return f===s;if(i?.has(u*(s+1)+f))return!1;let p=n[u];if(u===n.length-1){let d=t.slice(f);if(p.matchPart(d,r)!==void 0)return a[u]=d,!0}else if(p._tag===\"Literal\"){let d=o[u];if(t.startsWith(d,f)&&c(u+1,f+d.length))return a[u]=d,!0}else{let d=s-e.suffixLengths[u+1],m=o[u+1],x=m===void 0?d:t.lastIndexOf(m,d);for(;x>=f;){let y=t.slice(f,x);if(p.matchPart(y,r)!==void 0&&c(u+1,x))return a[u]=y,!0;if(x===0)break;x=m===void 0?x-1:t.lastIndexOf(m,x-1)}}return i??=new Set,i.add(u*(s+1)+f),!1}return c(0,0)?a:void 0}var HI=Fi(e=>{switch(e._tag){default:return e;case\"Number\":return e.toCodecStringTree();case\"Union\":return e.recur(HI)}}),Hh=Fi(e=>{switch(e._tag){default:return e;case\"Symbol\":case\"UniqueSymbol\":return e.toCodecStringTree();case\"Union\":return e.recur(Hh)}}),WI=Fi(e=>{switch(e._tag){default:return e;case\"Number\":case\"Literal\":case\"BigInt\":return e.toCodecStringTree();case\"Union\":return e.recur(WI)}}),mF=\"[\\\\s\\\\S]*?\",TI=new globalThis.RegExp(`^${Dh}$`),y6=new globalThis.RegExp(`^(?:${Dh}|Infinity|-Infinity|NaN)$`);function JI(e){return ap(TI,{expected:\"a string representing a finite number\",representation:{id:\"effect/schema/isStringFinite\",payload:null},toJsonSchema:()=>({pattern:TI.source}),...e})}var hF=Vo(pn,[JI()]),S6=new at(hF,pu),b6=new at(new zr([hF,rF]),pu),E6=\"-?\\\\d+\",wI=new globalThis.RegExp(`^${E6}$`);function KI(e){return ap(wI,{expected:\"a string representing a bigint\",representation:{id:\"effect/schema/isStringBigInt\",payload:null},toJsonSchema:()=>({pattern:wI.source}),...e})}var GI=Vo(pn,[KI({expected:\"a string representing a bigint\"})]),I6=new at(GI,Mh),T6=\"Symbol\\\\(([\\\\s\\\\S]*)\\\\)\",CI=new globalThis.RegExp(`^${T6}$`),w6=Vo(pn,[VI()]),xF=new at(w6,new ve(oe(e=>globalThis.Symbol.for(CI.exec(e)[1])),gt((e,t)=>globalThis.Symbol.keyFor(e)!==void 0?g(globalThis.String(e)):P(new Yl({message:\"cannot serialize to string, Symbol is not registered\"},e,t)))));function VI(e){return ap(CI,{expected:\"a string representing a symbol\",representation:{id:\"effect/schema/isStringSymbol\",payload:null},toJsonSchema:()=>({pattern:CI.source}),...e})}function no(e,t,r,n,o){for(let s=0;s0;){let o=n[n.length-1],s=o.keys;if(typeof s==\"number\"){if(o.index(e,t,r)=>Iu(e)?qe:P(new ft(t,e,r)),{representation:{id:\"effect/schema/Json\",payload:null},expected:\"JSON value\",toCodecJson:()=>{},toCodecStringTree:()=>YI}),bF=to(xu,{representation:{id:\"effect/schema/MutableJson\",payload:null}}),ZI=new at(xu,lu()),EF=new at(new zr([new Pn(!1,[],[xu]),new np([],[new MI(pn,xu)])]),lu());function k6(e){return SF(e,A6)}var v6=new yu([],()=>(e,t,r)=>k6(e)?qe:P(new ft(t,e,r)),{expected:\"StringTree\",toCodecStringTree:()=>{}}),YI=new at(v6,lu());var Wh=(e,t,r)=>e===qe?r(t):st(e,r);function TF(e){if(e._tag===\"Middleware\")return(r,n,o)=>{let s=r===qe?e.decode(Zt(Kl(n)),o):e.decode(Dr(r,Kl),o);return IF(s)};let t=e.decode;switch(t._tag){case\"Passthrough\":return(r,n)=>r===qe?Zt(n):r;case\"Transform\":{let r=n=>n===ge?$r:Zt(t.transform(n));return(n,o)=>Wh(n,o,r)}case\"TransformOptional\":{let r=n=>tI(t.transform(Kl(n)));return(n,o)=>Wh(n,o,r)}case\"TransformEffect\":return(r,n,o)=>Wh(r,n,s=>s===ge?$r:t.transform(s,o));case\"TransformOptionalEffect\":return(r,n,o)=>Wh(r,n,s=>IF(t.transform(Kl(s),o)))}}var IF=e=>st(e,tI),wF=(e,t,r,n)=>rt(n,o=>Is(()=>Ss(o,s=>new TP(e,s,t,r))));function O6(e,t){let r=TF(e.link.transformation),n;return(o,s)=>{if(o===ge)return $r;if(e.isConstructed(o))return qe;let a=(n??=t(e.link.to))(o,s);return r(a,o,s)}}function _6(e,t){let r=e.context.constructorDefault;return(n,o)=>{if(n!==ge&&n!==void 0)return t(n,o);let s=r;if(Ne(s)&&s._tag===\"Success\"){let a=t(s[ue],o);return a===qe?s:a}return st(wF(e,n,o,s),a=>{let i=t(a,o);return i===qe?Zt(a):i})}}function QI(e,t){let r=t(e);return e.context?.constructorDefault===void 0?r:_6(e,r)}function up(e,t,r,n,o){if(e._tag===\"Declaration\")for(let x of e.typeParameters)t(x);let s=r?yF(e):void 0,a=s?O6(s,t):n??e.getParser(t,r),i=e.checks,c=e.encoding,u=c?.map(x=>TF(x.transformation)),f=e.encodingChecks;if(!c&&!i&&!f)return a;let p,d=(x,y)=>{let S=a(x,y);if(f&&!y.disableChecks)if(Ne(S)){if(S._tag===\"Success\"){let w=S===qe?x:S[ue];if(x!==ge&&w!==ge){let E=no(f,x,void 0,e,y);E&&(S=P(new wt(e,E,x,y)))}}}else S=I(S,w=>{if(x!==ge&&w!==ge){let E=no(f,x,void 0,e,y);if(E)return P(new wt(e,E,x,y))}return g(w)});if(i&&!y.disableChecks)if(Ne(S)){if(S._tag===\"Success\"){let w=S===qe?x:S[ue];if(w===ge)return S;let E=no(i,w,void 0,e,y);E&&(S=P(new wt(e,E,w,y)))}}else S=I(S,w=>{if(w!==ge){let E=no(i,w,void 0,e,y);if(E)return P(new wt(e,E,w,y))}return g(w)});return S},m=o===void 0?d:o(d);return c?(x,y)=>{let S=p??=c.map(U=>t(U.to)),w=x,E=S[S.length-1](x,y);for(let U=c.length-1;U>=0;U--)if(E=u[U](E,w,y),U!==0){let _=S[U-1];E._tag===\"Success\"?(w=E[ue],E=_(w,y)):E=st(E,h=>{let b=_(h,y);return b===qe?Zt(h):b})}if(E._tag===\"Success\"){let U=E[ue],_=m(U,y);return _===qe?E:_}return E=wF(e,x,y,E),st(E,U=>{let _=m(U,y);return _===qe?Zt(U):_})}:m}var Gh=Symbol(),CF=new WeakMap,AF,Tu=!1;var P6=e=>Tu?Kh(gn,e,\"parser\"):gn(e).parser,kF=e=>Tu?Kh(gn,e,\"makeEffect\"):gn(e).makeEffect,M6=e=>QI(e,kF),Jh=class{ast;constructor(t){this.ast=t}get decodeEffect(){return this.cachedDecodeEffect??=up(this.ast,P6)}get parser(){return this.decodeEffect}get makeEffect(){return this.cachedMakeEffect??=up(this.ast,kF,M6)}},XI=class extends Jh{compiled;resolve;constructor(t,r,n){super(t),this.compiled=r,this.resolve=n}save(t,r){return Object.defineProperty(this,t,{value:r}),r}operation(t){let r=this.compiled;return typeof r==\"function\"?r(this.ast,this.resolve,t):r?.[t]}get is(){return this.save(\"is\",this.operation(\"is\"))}get decode(){return this.save(\"decode\",this.operation(\"decode\"))}get make(){return this.save(\"make\",this.operation(\"make\"))}get decodeEffect(){return this.save(\"decodeEffect\",this.operation(\"decodeEffect\")??up(this.ast,t=>Kh(this.resolve,t,\"parser\")))}get parser(){let t=this.decode;return t===void 0?this.decodeEffect:this.save(\"parser\",F6(t,()=>this.decodeEffect))}get makeEffect(){let t=this.operation(\"makeEffect\");if(t!==void 0)return this.save(\"makeEffect\",t);let r=n=>Kh(this.resolve,n,\"makeEffect\");return this.save(\"makeEffect\",up(this.ast,r,n=>QI(n,r)))}};function F6(e,t){let r;return(n,o)=>{if(n!==ge)try{let s=e(n,o);if(s!==Gh)return s===n?qe:Zt(s)}catch(s){return an(s)}return(r??=t())(n,o)}}function Kh(e,t,r){let n=e(t);if(n.compiled===void 0||Object.hasOwn(n,r))return n[r];let o;return(s,a)=>(o??=n[r])(s,a)}function gn(e){let t=CF.get(e);if(t!==void 0)return t;let r=AF===void 0?new Jh(e):new XI(e,AF(e,gn),gn);return CF.set(e,r),r}function tT(e){let t=e.ast,r;return(n,o)=>(r??=cT(q6,yt(t)))(n,o?.disableChecks?o?.parseOptions?{...o.parseOptions,disableChecks:!0}:{disableChecks:!0}:o?.parseOptions)}function Zh(e){let t=tT(e);return(r,n)=>{let o=Wo(t(r,n));return Ve(o)?O(o.value):(ln(o.cause,\"Option adapter can only return none for schema issues\"),v())}}function Yh(e){return j6(yt(e.ast))}function rT(e){return Ui(e.ast)}function U6(e){if(!Tu){let o=wu(ex(e));return s=>{let a=o(s,Go);return Ve(a)?!0:(ln(a.cause,\"Type guard adapter can only return false for schema issues\"),!1)}}let t=gn(e),r=t.is;if(r!==void 0)return o=>{try{return r(o,Go)}catch(s){return ln(ci(s),\"Type guard adapter can only return false for schema issues\"),!1}};let n=t.parser;return o=>{let s=Wo(iT(n(o,Go),o));return Ve(s)?!0:(ln(s.cause,\"Type guard adapter can only return false for schema issues\"),!1)}}function Ui(e){let t=yt(e),r=n=>(r=U6(t),r(n));return n=>r(n)}function vF(e){let t=ex(e);return(r,n)=>{let o=Wo(t(r,n));if(!Ve(o))return ln(o.cause,\"Issue adapter can only return schema issues\")}}function OF(e,t){let r=yt(e.ast),n=gn(r).parser(t,Go),o=Wo(iT(n,t));if(Uo(o)){let s=ln(o.cause,\"Assertion adapter can only throw schema issues\");throw new Error(\"Schema validation failed\",{cause:s})}}function nt(e,t){let r=ex(e.ast);return t===void 0?r:(n,o)=>r(n,aT(t,o))}var nT=nt;function _F(e,t){return wu(nt(e,t))}function oT(e,t){return DF(nt(e,t))}var RF=oT;function PF(e,t){return LF(nt(e,t))}function D6(e,t){return Tu?$F(e.ast,t):uT(nt(e,t))}var Qh=D6;function Di(e,t){let r=ex(mn(e.ast));return t===void 0?r:(n,o)=>r(n,aT(t,o))}var MF=Di;function FF(e,t){return wu(Di(e,t))}function sT(e,t){return DF(Di(e,t))}var NF=sT;function UF(e,t){return LF(Di(e,t))}function L6(e,t){return Tu?$F(mn(e.ast),t):uT(Di(e,t))}var Xh=L6,aT=(e,t)=>t?{...e,...t}:e,Vh=e=>e===ge?P(new de):g(e);function ex(e){return cT(z6,e)}function iT(e,t){return e===qe?g(t):Ne(e)?e[ue]===ge?Vh(ge):e:st(e,Vh)}function cT(e,t){let r;return(n,o)=>{let s=(r??=e(t))(n,o??Go);return s===qe?g(n):Ne(s)?s[ue]===ge?Vh(ge):s:st(s,Vh)}}function wu(e){return(t,r)=>Wo(e(t,r))}function DF(e){let t=wu(e);return(r,n)=>{let o=t(r,n);return Ve(o)?O(o.value):(ln(o.cause,\"Option adapter can only return none for schema issues\"),v())}}function LF(e){let t=wu(e);return(r,n)=>{let o=t(r,n);return Ve(o)?B(o.value):W(ln(o.cause,\"Result adapter can only return schema issues\"))}}function $F(e,t){let r;return(n,o)=>(r??=$6(gn(e),t))(n,o)}function $6(e,t){let r=e.decode,n,o=r===void 0?(s,a=Go)=>(n??=eT(e))(s,a):(s,a=Go)=>{if(s===ge)return(n??=eT(e))(s,a);let i;try{i=r(s,a)}catch(c){throw ln(ci(c),\"Sync adapter can only throw schema issues\"),c}return i!==Gh?i:(n??=eT(e))(s,a)};return t===void 0?o:(s,a)=>o(s,aT(t,a))}function eT(e){return uT(cT(()=>e.decodeEffect,e.ast))}function uT(e){let t=wu(e);return(r,n)=>{let o=t(r,n);if(Ve(o))return o.value;let s=ln(o.cause,\"Sync adapter can only throw schema issues\");throw new Error(\"Schema validation failed\",{cause:s})}}function B6(e,t){let r=Wo(e);if(Ve(r))return r.value;let n=ln(r.cause,t);throw new Error(\"Schema validation failed\",{cause:n})}function j6(e){let t,r;return(n,o)=>{t??=gn(e);let s=o?.disableChecks?o.parseOptions?{...o.parseOptions,disableChecks:!0}:{disableChecks:!0}:o?.parseOptions??Go,a=t.make;if(a!==void 0&&n!==ge){let c;try{c=a(n,s)}catch(u){throw ln(ci(u),\"Constructor adapter can only throw schema issues\"),u}if(c!==Gh&&c!==ge)return c}let i=(r??=t.makeEffect)(n,s);return B6(iT(i,n),\"Constructor adapter can only throw schema issues\")}}var z6=e=>gn(e).parser,q6=e=>gn(e).makeEffect;var tx=\"~effect/Schema/Schema\",BF=Symbol(),H6={[tx]:tx,get make(){let e=Yh(this);return Object.defineProperty(this,\"make\",{value:e,enumerable:!0}),e},get makeEffect(){let e=tT(this);return Object.defineProperty(this,\"makeEffect\",{value:e,enumerable:!0}),e},get makeOption(){let e=Zh(this);return Object.defineProperty(this,\"makeOption\",{value:e,enumerable:!0}),e},pipe(){return Z(this,arguments)},annotate(e){return this.rebuild(to(this.ast,e))},annotateKey(e){return this.rebuild(cp(this.ast,e))},check(...e){return this.rebuild(Vo(this.ast,e))},rebuild(e){return oo(e,this[BF])}};function oo(e,t){function r(){}let n=Object.setPrototypeOf(r,H6);return t&&(Object.hasOwn(t,\"name\")||Object.hasOwn(t,\"length\")||Object.hasOwn(t,\"__proto__\"))?Object.defineProperties(n,Object.getOwnPropertyDescriptors({...t})):Object.assign(n,t),n[BF]=t,n.ast=e,n}function fT(e){return e.replace(/~/g,\"~0\").replace(/\\//g,\"~1\")}function fp(e){return e.replace(/~1/g,\"/\").replace(/~0/g,\"~\")}function lT(e){return encodeURI(fT(e)).replace(/#/g,\"%23\")}function pT(e){if(e.length===0||e===\"#\")return\"\";if(!e.startsWith(\"#\"))return;let t=e.slice(1);try{if(encodeURI(t).replace(/%25/g,\"%\").replace(/#/g,\"%23\")!==t)return;let r=decodeURIComponent(t);return r.startsWith(\"/\")&&!/~(?:[^01]|$)/.test(r)?r:void 0}catch{return}}function zF(e){let t=pT(e);return t===void 0?void 0:t.length===0?[]:t.slice(1).split(\"/\").map(fp)}function qF(e){return e.reduce((t,r)=>`${t}/${lT(r)}`,\"#\")}var J6=\"http://json-schema.org/draft-07/schema#\",HF=\"https://json-schema.org/draft/2020-12/schema\";function WF(e,t){return e===t||e===(t.endsWith(\"#\")?t.slice(0,-1):`${t}#`)}function GF(e){return{dialect:\"draft-07\",...Y6(e,iG)}}function K6(e,t){return r(e,!1,!0);function r(n,o,s=!1){if(!fr(n))return n;let a=o||!s&&YF(n.$id),i={};for(let c of Object.keys(n)){let u=n[c],f=u;switch(c){case\"$defs\":case\"properties\":case\"patternProperties\":case\"dependentSchemas\":f=mT(u,p=>r(p,a))??u;break;case\"allOf\":case\"anyOf\":case\"oneOf\":case\"prefixItems\":f=Array.isArray(u)?u.map(p=>r(p,a)):u;break;case\"not\":case\"additionalProperties\":case\"propertyNames\":case\"unevaluatedProperties\":case\"items\":case\"contains\":case\"unevaluatedItems\":case\"if\":case\"then\":case\"else\":case\"contentSchema\":f=r(u,a)}M(i,c,f)}return t(i,a),i}}function VF(e,t){return K6(e,r=>{G6(r,t)})}function G6(e,t){typeof e.$ref==\"string\"&&M(e,\"$ref\",t(e.$ref,\"$ref\")),typeof e.$dynamicRef==\"string\"&&M(e,\"$dynamicRef\",t(e.$dynamicRef,\"$dynamicRef\"))}function mT(e,t){if(!fr(e))return;let r={};for(let n of Object.keys(e))M(r,n,t(e[n],n));return r}function V6(e,t,r){let n=new Map,o=[],s=dT;function a(d,m=[],x=[]){if(m.length===0&&t?.trackIds){let y=fr(d)?f(d):void 0;s=JF(y,dT)??dT}return u(d,m,x,{sourceRoot:[],targetRoot:[],uri:s})}function i(){for(let[d,m,x]of o){let y=m,S=ZF(m,x);if(S!==void 0){let w=zF(S.hash);if(S.hash=\"\",w!==void 0){let E=n.get(KF(S.href,w));E!==void 0&&(y=Z6(m,E))}}M(d,\"$ref\",y)}}let c=r(a);return i(),c;function u(d,m,x,y){if(typeof d==\"boolean\")return p(m,x,y),t?.booleanAdapter?.(d)??d;if(!fr(d))return d;let S=y,w=f(d);if(m.length>0&&t?.trackIds&&YF(w)){let _=JF(w,y.uri);_!==void 0&&(S={parent:y,sourceRoot:m,targetRoot:x,uri:_})}p(m,x,S);let E=S.uri,U={isDocumentRoot:m.length===0,schema(_,h,b=h){return u(_,[...m,h],[...x,b],S)},schemaAt(_,h,b){return u(_,[...m,...h],[...x,...b],S)},schemaArray(_,h,b=h){return Array.isArray(_)?_.map((T,C)=>u(T,[...m,h,String(C)],[...x,b,String(C)],S)):_},schemaMap(_,h,b=h){return fr(_)?mT(_,(T,C)=>u(T,[...m,h,C],[...x,b,C],S)):_},reference(_,h){typeof h==\"string\"?o.push([_,h,E]):M(_,\"$ref\",h)}};return e(d,U)}function f(d){return t?.ignoreRefSiblings===!0&&typeof d.$ref==\"string\"?void 0:d.$id}function p(d,m,x){x.parent!==void 0&&p(d,m,x.parent),n.set(KF(x.uri,d.slice(x.sourceRoot.length)),m.slice(x.targetRoot.length))}}var dT=\"https://effect.invalid/.json-schema/\";function ZF(e,t){return URL.canParse(e,t)?new URL(e,t):void 0}function JF(e,t){if(typeof e!=\"string\")return;let r=ZF(e,t);if(r!==void 0)return r.hash=\"\",r.href}function Z6(e,t){let r=e.indexOf(\"#\");return r===-1&&t.length===0?e:`${r===-1?e:e.slice(0,r)}${qF(t)}`}function KF(e,t){return`${e}\\0${JSON.stringify(t)}`}function YF(e){return typeof e==\"string\"&&e.length>0&&e[0]!==\"#\"}function Y6(e,t,r){return V6(t,{...r,trackIds:!0},n=>({schema:n(e.schema),definitions:mT(e.definitions,(o,s)=>n(o,[\"$defs\",s],[\"definitions\",s]))}))}var Q6=new Set([\"properties\",\"patternProperties\"]),X6=new Set([\"allOf\",\"anyOf\",\"oneOf\"]),eG=new Set([\"not\",\"additionalProperties\",\"propertyNames\",\"contains\",\"if\",\"then\",\"else\",\"contentSchema\"]);function tG(e,t,r,n,o,s=Q6){let a;if(s.has(t))a=n.schemaMap(r,t);else if(X6.has(t))a=n.schemaArray(r,t);else if(o.has(t))a=n.schema(r,t);else return!1;return M(e,t,a),!0}var QF=[\"$anchor\",\"$defs\",\"$dynamicAnchor\",\"$dynamicRef\",\"$vocabulary\",\"contentSchema\",\"dependentRequired\",\"dependentSchemas\",\"maxContains\",\"minContains\",\"prefixItems\",\"unevaluatedItems\",\"unevaluatedProperties\"],Iie=[...QF,\"deprecated\"],Tie=[...QF,\"$comment\",\"$id\",\"$schema\",\"const\",\"contains\",\"contentEncoding\",\"contentMediaType\",\"else\",\"examples\",\"if\",\"patternProperties\",\"propertyNames\",\"then\"],rG=/^[A-Za-z_][-A-Za-z0-9._]*$/,nG=/^[A-Za-z][-A-Za-z0-9._:]*$/;function Li(e,t,r){throw new Error(`Cannot convert JSON Schema keyword \"${e}\" to ${t}: ${r}`)}function oG(e,t,r,n){for(let o of t)Object.hasOwn(e,o)&&Li(o,r,`it is not active in ${n} but would become active in the target`)}var sG=[\"additionalItems\",\"definitions\",\"dependencies\"];function aG(e,t,r,n,o){r.isDocumentRoot?M(e,\"$schema\",WF(t,HF)?n:t):WF(t,HF)||Li(\"$schema\",o,\"an embedded resource cannot declare a different dialect\")}function iG(e,t){oG(e,sG,\"Draft-07\",\"Draft 2020-12\");let r={},n,o,s;for(let a of Object.keys(e)){let i=e[a];if(!tG(r,a,i,t,eG))switch(a){case\"$ref\":n=i;break;case\"$schema\":aG(r,i,t,J6,\"Draft-07\");break;case\"$id\":case\"$anchor\":break;case\"$defs\":M(r,\"definitions\",t.schemaMap(i,a,\"definitions\"));break;case\"prefixItems\":o=i;break;case\"items\":s=i;break;case\"dependentRequired\":case\"dependentSchemas\":case\"minContains\":case\"maxContains\":break;case\"$dynamicRef\":case\"$dynamicAnchor\":case\"$vocabulary\":case\"unevaluatedProperties\":case\"unevaluatedItems\":Li(a,\"Draft-07\",\"the target dialect has no equivalent\");case\"required\":if(Array.isArray(i)&&i.length===0)break;M(r,a,i);break;default:M(r,a,i)}}if(cG(r,o,s,t),Object.hasOwn(e,\"contains\")){let a=e.minContains,i=e.maxContains;(a!==void 0&&a!==1||i!==void 0)&&Li(\"minContains/maxContains\",\"Draft-07\",\"contains cardinality cannot be represented\"),Object.hasOwn(e,\"minContains\")&&M(r,\"minContains\",a)}else Object.hasOwn(e,\"minContains\")&&M(r,\"minContains\",e.minContains),Object.hasOwn(e,\"maxContains\")&&M(r,\"maxContains\",e.maxContains);return fG(e,r,t,\"draft-07\"),pG(e,r,\"$id\",\"Draft-07\"),uG(r,n,t),r}function cG(e,t,r,n){if(t===void 0){r!==void 0&&M(e,\"items\",n.schema(r,\"items\"));return}M(e,\"items\",n.schemaArray(t,\"prefixItems\",\"items\")),r!==void 0&&M(e,\"additionalItems\",n.schema(r,\"items\",\"additionalItems\"))}function uG(e,t,r){if(t!==void 0)if(typeof t==\"string\"&&Object.keys(e).length>0){let n={};r.reference(n,t),lG(e,n)}else r.reference(e,t)}function fG(e,t,r,n){let o=fr(e.dependentRequired)?e.dependentRequired:void 0,s=fr(e.dependentSchemas)?e.dependentSchemas:void 0;if(o===void 0&&s===void 0)return;let a={},i=new Set([...Object.keys(o??{}),...Object.keys(s??{})]);for(let c of i){let u=o?.[c],f=s?.[c],p=n===\"draft-04\"&&Array.isArray(u)&&u.length===0;f===void 0?p||M(a,c,u):u===void 0||p?M(a,c,r.schemaAt(f,[\"dependentSchemas\",c],[\"dependencies\",c])):M(a,c,{allOf:[r.schemaAt(f,[\"dependentSchemas\",c],[\"dependencies\",c,\"allOf\",\"0\"]),{required:u}]})}Object.keys(a).length>0&&M(t,\"dependencies\",a)}function lG(e,t){Array.isArray(e.allOf)?e.allOf.push(t):M(e,\"allOf\",[t])}function pG(e,t,r,n){let o=e.$id,s=e.$anchor;if(s===void 0){o!==void 0&&M(t,r,o);return}(typeof s!=\"string\"||!rG.test(s))&&Li(\"$anchor\",n,\"the anchor is not valid\"),nG.test(s)||Li(\"$anchor\",n,\"the anchor cannot be represented as a plain-name fragment identifier\"),o===void 0?M(t,r,`#${s}`):Li(\"$anchor\",n,\"it cannot be combined with the schema $id\")}function rx(e){return oo(Cu(e.ast),{schema:e})}var Cu=Fi(e=>{let t=mG(e,Cu),r=e.context;return t===e||r===void 0?t:zh(t,hT(r))});function hT(e){return e.constructorDefault===void 0?e:new Ko(e.isOptional,e.isMutable,void 0,e.annotations)}function tN(e){if(e.propertySignatures.some(t=>typeof t.name!=\"string\"))throw new globalThis.Error(\"Objects property names must be strings\",{cause:e})}function rN(e){return t=>{let r=[...t].sort((o,s)=>e(hn(o))-e(hn(s)));return r.some((o,s)=>o!==t[s])?r:t}}var dG=rN(e=>{switch(e._tag){case\"BigInt\":case\"Symbol\":case\"UniqueSymbol\":return 0;default:return 1}});function mG(e,t){switch(e._tag){case\"Declaration\":{let r=e.annotations?.toCodecJson??e.annotations?.toCodec;if(!Zr(r))return We(e,[ZI]);let n=e.typeParameters.map(s=>oo(hn(s))),o=r(n);return o===void 0?e:We(e,[Eu(o,t)])}case\"Unknown\":return We(e,[ZI]);case\"ObjectKeyword\":return We(e,[EF]);case\"Undefined\":case\"Void\":case\"Literal\":case\"Number\":return e.toCodecJson();case\"UniqueSymbol\":case\"Symbol\":case\"BigInt\":return e.toCodecStringTree();case\"Objects\":return tN(e),e.recur(t,Hh);case\"Union\":{let r=dG(e.types);return r!==e.types?new zr(r,e.options,e.annotations,e.checks,e.encoding,e.context,e.encodingChecks).recur(t):e.recur(t)}case\"Arrays\":case\"Suspend\":return e.recur(t)}return e}function nx(e){return oo(nN(yt(e.ast)))}var nN=Vr(e=>{let t=hG(e,nN);return t!==e&&e.context!==void 0?zh(t,hT(e.context)):t});function hG(e,t){switch(e._tag){case\"Declaration\":{let r=e.annotations?.toCodecIso??e.annotations?.toCodec;if(Zr(r)){let n=r(e.typeParameters.map(o=>oo(o)));return We(e,[Eu(n,t)])}return e}case\"Arrays\":case\"Objects\":case\"Union\":case\"Suspend\":return e.recur(t)}return e}function ox(e){return oo(iN(e.ast),{schema:e})}function oN(e){return oo(cN(e.ast))}var xG=rN(e=>{switch(e._tag){case\"Null\":case\"Boolean\":case\"Number\":case\"BigInt\":case\"Symbol\":case\"UniqueSymbol\":return 0;default:return 1}});function gG(e,t,r){switch(e._tag){case\"Declaration\":{let n=e.typeParameters.map(u=>oo(t(hn(u)))),o=e.annotations?.toCodecStringTree;if(Zr(o)){let u=o(n);return u===void 0?e:We(e,[Eu(u,t)])}let s=e.annotations?.toCodecJson,a=Zr(s)?s(n):void 0,i=a===void 0?e.annotations?.toCodec:void 0,c=a??(Zr(i)?i(n):void 0);return c===void 0?r(e):We(e,[Eu(c,t)])}case\"Null\":return We(e,[yG]);case\"Boolean\":return We(e,[SG]);case\"Unknown\":case\"ObjectKeyword\":return We(e,[YI]);case\"Enum\":case\"Number\":case\"Literal\":case\"UniqueSymbol\":case\"Symbol\":case\"BigInt\":return e.toCodecStringTree();case\"Objects\":return tN(e),e.recur(t,Hh);case\"Union\":{let n=xG(e.types);return n!==e.types?new zr(n,e.options,e.annotations,e.checks,e.encoding,e.context,e.encodingChecks).recur(t):e.recur(t)}case\"Arrays\":case\"Suspend\":return e.recur(t)}return e}var yG=new at(new dn(\"null\"),new ve(oe(()=>null),oe(()=>\"null\"))),SG=new at(new zr([new dn(\"true\"),new dn(\"false\")]),new ve(oe(e=>e===\"true\"),_i())),sN=new ve(oe(e=>typeof e==\"string\"?[e]:e),jr()),aN=e=>e.transformation===sN,iN=Fi(e=>{let t=gG(e,iN,r=>{throw new globalThis.Error(\"Missing structural codec for StringTree\",{cause:r})});return t!==e&&e.context!==void 0?zh(t,hT(e.context)):t},{stopAt:aN}),eN=e=>Qt(e)?Ni(eo):eo,cN=Fi(e=>{let t=bG(e);if(AI(t)){let r=ga(new zr([new Pn(t.isMutable,t.elements.map(eN),t.rest.map(eN)),pn],{mode:\"anyOf\"}),t,sN);return Qt(e)?Ni(r):r}return t},{stopAt:aN});function bG(e){return e._tag===\"Declaration\"||e._tag===\"Arrays\"||e._tag===\"Objects\"||e._tag===\"Union\"||e._tag===\"Suspend\"?e.recur(cN):e}var Cie=globalThis.RegExp;var ks=e=>e.replace(/[/\\\\^$*+?.()|[\\]{}]/g,\"\\\\$&\");function ya(e,t){return t.length>0&&(e+=`\n at ${pd(t)}`),new Error(e)}function uN(e){return`#/$defs/${lT(e)}`}var EG=new Set([...EP,vi,...bP]);function pp(e,t){if(e===void 0)return;let r={},n=e.title;typeof n==\"string\"&&(r.title=n);let o=e.description,s=e.expected;typeof o==\"string\"?r.description=o:t?.generateDescriptions===!0&&typeof s==\"string\"&&(r.description=s);let a=e.default;Iu(a)&&(r.default=a);let i=e.examples;Array.isArray(i)&&Iu(i)&&(r.examples=i);let c=e.readOnly;typeof c==\"boolean\"&&(r.readOnly=c);let u=e.writeOnly;typeof u==\"boolean\"&&(r.writeOnly=u);let f=e.format;typeof f==\"string\"&&(r.format=f);let p=e.contentEncoding;typeof p==\"string\"&&(r.contentEncoding=p);let d=e.contentMediaType;typeof d==\"string\"&&(r.contentMediaType=d);let m=e.contentSchema;if(Iu(m)&&(r.contentSchema=m),t?.includeAnnotationKey!==void 0)for(let[x,y]of Object.entries(e))EG.has(x)||!t.includeAnnotationKey(x)||Iu(y)&&M(r,x,y);return Object.keys(r).length===0?void 0:r}function lN(e){let t=e.type===\"number\"||e.type===\"integer\"?e.type:void 0,r=e;if(t!==void 0&&(r={...e},delete r.type),Array.isArray(r.allOf)){let n=[],o=!1;for(let s of r.allOf){let a=lN(s);a.type!==void 0&&(o=!0,(t===void 0||a.type===\"integer\")&&(t=a.type)),Object.keys(a.schema).length>0&&n.push(a.schema)}if(o){let{allOf:s,...a}=r;r=n.length===0?a:{...a,allOf:n}}}return{type:t,schema:r}}function IG(e){if(!Array.isArray(e.anyOf)||e.anyOf.length!==2||e.anyOf[0]?.type!==\"number\")return!1;let t=e.anyOf[1];return t?.type===\"string\"&&Array.isArray(t.enum)&&t.enum.length===3&&t.enum.includes(\"NaN\")&&t.enum.includes(\"Infinity\")&&t.enum.includes(\"-Infinity\")}var pN=\"|type|format|pattern|multipleOf|minimum|maximum|exclusiveMinimum|exclusiveMaximum|minLength|maxLength|minItems|maxItems|uniqueItems|minProperties|maxProperties|propertyNames|\";function sx(e,t){return Object.keys(e).every(r=>t.includes(`|${r}|`))}function fN(e,t){return typeof e.$ref!=\"string\"&&t.every(r=>!Object.hasOwn(e,r))}var yT=\"|title|description|default|examples|readOnly|writeOnly|\",TG=pN+yT;function dp(e,t,r){if(Object.keys(e).length===0)return t;let n=Object.keys(t);if(n.length===0)return e;let o=e.type===\"number\"||e.type===\"integer\"?e.type:void 0,s=IG(e);if(o!==void 0||s){let i=lN(t);if(i.type!==void 0){let c=o===\"integer\"||i.type===\"integer\"?\"integer\":\"number\",u={...e,type:c};s&&delete u.anyOf;let f=Object.keys(i.schema);return f.length===0?u:sx(i.schema,yT)&&fN(u,f)?{...u,...i.schema}:dp(u,i.schema,r)}}if(r&&fN(e,n))return{...e,...t};let a=Array.isArray(t.allOf)&&n.length===1?t.allOf:[t];return Array.isArray(e.allOf)?{...e,allOf:[...e.allOf,...a]}:typeof e.$ref==\"string\"?{allOf:[e,...a]}:{...e,allOf:a}}function gT(e,t=new Set){return typeof e==\"boolean\"?e:t.has(e)?!0:(t.add(e),e.every(r=>gT(r,t)))}function wG(e){return Array.isArray(e)}function CG(e,t,r,n){let o=new Map,s=new Map,a=new WeakMap,i=new Map,c=new Map,u=Object.keys(r);for(let h of u)x(h,[\"references\",h]);let f=wn(e,(h,b)=>E(h,t[b],[]));for(let[h,b]of i.values())if(!gT(h))for(let T of b)Array.isArray(T.oneOf)&&(T.anyOf=T.oneOf,delete T.oneOf);for(let h of u){let b=r[h],T=S(b);if(T===void 0)continue;let C=o.get(h)[0],L=c.get(T),ee=L?.find(ye=>J(o.get(ye)[0],C));ee===void 0?L===void 0?c.set(T,[h]):L.push(h):s.set(h,ee)}let p=wn(f,y),d={};for(let h of u)s.has(h)||M(d,h,y(o.get(h)[0]));return{dialect:\"draft-2020-12\",schemas:p,definitions:d};function m(h){return Array.isArray(h.oneOf)&&i.get(h.oneOf)?.[1].push(h),h}function x(h,b){if(!Object.hasOwn(r,h))throw ya(`Invalid reference ${h}`,[...b,\"$ref\"]);let T=o.get(h);if(T!==void 0)return T;let C=[{},[]];return o.set(h,C),C[0]=E(r[h],[\"references\",h],C[1]),C}function y(h){return VF(h,b=>{let T=pT(b);if(T===void 0){if(/^#(?:\\/|%2f)/i.test(b))throw new Error(`Invalid JSON Pointer URI fragment ${JSON.stringify(b)}`);return b}if(!T.startsWith(\"/$defs/\"))return b;let C=T.indexOf(\"/\",7),L=s.get(fp(T.slice(7,C<0?void 0:C)));return L===void 0?b:b.replace(/^#(?:\\/|%2f).*?(?:\\/|%2f).*?(?=\\/|%2f|$)/i,uN(L))})}function S(h){if(h._tag===\"Reference\")return;let b=h.checks.length===0?h.annotations:h.checks[h.checks.length-1].annotations;return typeof b?.identifier!=\"string\"&&typeof b?.[vi]==\"string\"?b[vi]:void 0}function w(h,b,T,C){let L=h.annotations,ee=L?.toJsonSchema;if(ee!==void 0){let Ze=h.representation?.schemas?.map((gf,X$)=>E(gf,[...T,\"representation\",\"schemas\",X$],C))??[],Ls=ee({type:b,schemas:Ze}),et=wG(Ls);et&&C.push(!1);let zt=et?Ls[0]:Ls,ur=pp(L,n),ss=ur===void 0?zt:{...zt,...ur},_a=ur===void 0?pN:TG;return[ss,h._tag===\"Filter\"&&sx(ss,_a)&&(ur===void 0||sx(ur,yT))?!0:void 0]}if(h._tag===\"Filter\"){C.push(!1);return}let ye=h.checks.flatMap((Ze,Ls)=>{let et=w(Ze,b,[...T,\"checks\",Ls],C);return et===void 0?[]:[et[0]]});if(ye.length===0)return;let _e=pp(L,n),Et={allOf:ye};return[_e===void 0?Et:{...Et,..._e}]}function E(h,b,T){if(h._tag===\"Reference\")return T.push(x(h.$ref,b)[1]),{$ref:uN(h.$ref)};let C=a.get(h);if(C!==void 0)return T.push(C[1]),C[0];let L=[];T.push(L);let ee=U(h,b,L),ye=pp(h.annotations,n);ye!==void 0&&(ee={...ee,...ye});for(let _e=0;_e{if(typeof ee==\"number\"&&!globalThis.Number.isFinite(ee))throw ya(`Invalid numeric enum value ${globalThis.String(ee)}`,[...b,\"enums\",ye,1]);return{type:typeof ee,enum:[ee],title:L}});return C.length===0?{not:{}}:{anyOf:C}}case\"TemplateLiteral\":return{type:\"string\",pattern:`^${h.parts.map(ax).join(\"\")}$`};case\"Arrays\":{if(h.rest.length>1)throw ya(\"Invalid schema representation document\",[...b,\"rest\"]);let C={type:\"array\"},L=h.elements.length,ee=h.elements.map((ye,_e)=>{ye.isOptional&&L--;let Et=E(ye.type,[...b,\"elements\",_e,\"type\"],T),Ze=pp(ye.annotations,n);return Ze===void 0?Et:m(dp(Et,Ze))});if(ee.length>0?(C.prefixItems=ee,C.maxItems=h.elements.length,L>0&&(C.minItems=L)):C.items=!1,h.rest.length===1){delete C.maxItems;let ye=E(h.rest[0],[...b,\"rest\",0],T);Object.keys(ye).length>0?C.items=ye:delete C.items}return C}case\"Objects\":{if(h.propertySignatures.length===0&&h.indexSignatures.length===0)return{not:{type:\"null\"}};let C={type:\"object\"},L={},ee=[];for(let et=0;et0&&(C.properties=L),ee.length>0&&(C.required=ee);let ye={},_e=[],Et=[],Ze=!1;for(let et=0;et0;if(Ls&&(C.patternProperties=ye),Ze&&n?.onExcessProperty===\"error\"){let et=[];h.propertySignatures.length>0&&et.push({enum:Object.keys(L)});for(let zt=0;zt0&&(C.allOf=_e.map(zt=>({type:\"object\",additionalProperties:zt})))}else _e.length===0?C.additionalProperties=Ze||n?.onExcessProperty!==\"error\":_e.length===1&&h.propertySignatures.length===0&&!Ls?C.additionalProperties=_e[0]:_e.length>0&&(C.allOf=_e.map(et=>({type:\"object\",additionalProperties:et})));return typeof C.additionalProperties==\"object\"&&C.additionalProperties!==null&&Object.keys(C.additionalProperties).length===0&&delete C.additionalProperties,C}case\"Union\":{let C=[],L=h.types.map((ye,_e)=>E(ye,[...b,\"types\",_e],C));if(T.push(C),L.length===0)return{not:{}};let ee=h.options?.mode??\"anyOf\";if(ee===\"anyOf\"&&L.length>1){let ye=kG(L);if(ye!==void 0)return ye}return ee===\"oneOf\"&&i.set(L,[C,[]]),{[ee]:L}}}}function _(h,b,T){switch(h._tag){case\"Reference\":{if(!Object.hasOwn(r,h.$ref))throw ya(`Invalid reference ${h.$ref}`,[...b,\"$ref\"]);if(x(h.$ref,b),T.has(h.$ref))return;let C=new Set(T).add(h.$ref);return _(r[h.$ref],[\"references\",h.$ref],C)}case\"String\":{if(h.checks.length===0)return[];if(h.checks.length!==1||h.checks[0]._tag!==\"Filter\"||h.checks[0].annotations?.toJsonSchema===void 0)return;let C=[],L=E(h,b,C);return gT(C)&&L.type===\"string\"&&typeof L.pattern==\"string\"&&sx(L,\"|type|pattern|title|description|default|examples|readOnly|writeOnly|\")?[L.pattern]:void 0}case\"TemplateLiteral\":return h.checks.length===0?[`^${h.parts.map(ax).join(\"\")}$`]:void 0;case\"Union\":{if(h.types.length===0||h.checks.length>0||h.options?.mode===\"oneOf\")return;let C=[];for(let L=0;Le;function so(e){return e===void 0?void 0:{annotations:e}}function mp(e,t){let{references:r,representations:n}=OG([e],t);return{representation:n[0],references:r}}function OG(e,t){let r={},n=new Map,o=new Set,s=new Map,a=new Set;for(let h of e)m(h);let i=t?.referencePolicy??vG;for(let h of s.values())for(let b of h.values()){let T=i({ast:b.ast,occurrences:b.occurrences,identifier:b.identifier});if(T!==void 0){let C=T===b.identifier||!T.endsWith(\"_\")?\"_\":\"\";b.reference=u(T,b,C)}else b.isRecursive&&(b.reference=u(`${b.ast._tag}_`,b,\"\"))}return{representations:wn(e,h=>y(h)),references:r};function u(h,b,T=\"_\"){let C=h,L=0;for(;n.has(C);){if(n.get(C)===b)return C;C=`${h}${T}${++L}`}return n.set(C,b),C}function f(h,b,T){let C=b.fallback;return C!==void 0?XE(h)===C?h:to(h,{[vi]:C}):T===b.identifier?h:to(h,{identifier:T})}function p(h,b){if(!Object.hasOwn(r,h)&&!o.has(h)){o.add(h);let T=S(b);o.delete(h),M(r,h,T)}return{_tag:\"Reference\",$ref:h}}function d(h){let b=bu(h),T=Su(b),C=iu(b),L=C===void 0?(b!==h?iu(h):void 0)??XE(b):void 0;L!==void 0&&(C=`${L}Encoded`);let ee=s.get(T);ee===void 0&&(ee=new Map,s.set(T,ee));let ye=ee.get(C);return ye===void 0&&(ye={ast:T,identifier:C,fallback:L,occurrences:0,isRecursive:!1,reference:void 0},ee.set(C,ye)),ye}function m(h){let b=d(h),T=b.ast;if(b.occurrences++,a.has(b)){b.isRecursive=!0;return}if(!(b.occurrences>1)){switch(a.add(b),x(T.checks),T._tag){case\"Declaration\":case\"Arrays\":case\"Objects\":case\"Union\":T.recur(C=>(m(C),C));break;case\"TemplateLiteral\":T.parts.forEach(m);break;case\"Suspend\":m(T.thunk());break}a.delete(b)}}function x(h){h?.forEach(b=>{b.annotations?.representation?.schemas?.forEach(T=>m(yt(T))),b._tag===\"FilterGroup\"&&x(b.checks)})}function y(h){let b=d(h),T=b.ast,C=b.reference;if(C!==void 0){let L=b.identifier===void 0?T:f(T,b,C);return p(C,L)}return S(T)}function S(h){let b=w(h.checks);switch(h._tag){case\"Declaration\":return{_tag:\"Declaration\",typeParameters:h.typeParameters.map(T=>y(T)),checks:b,...U(h.annotations)};case\"Null\":case\"Undefined\":case\"Void\":case\"Never\":case\"Unknown\":case\"Any\":case\"String\":case\"Boolean\":case\"Number\":case\"BigInt\":case\"Symbol\":case\"ObjectKeyword\":return{_tag:h._tag,checks:b,...so(h.annotations)};case\"Literal\":return{_tag:\"Literal\",literal:h.literal,checks:b,...so(h.annotations)};case\"UniqueSymbol\":return{_tag:\"UniqueSymbol\",symbol:h.symbol,checks:b,...so(h.annotations)};case\"Enum\":return{_tag:\"Enum\",enums:h.enums,checks:b,...so(h.annotations)};case\"TemplateLiteral\":return{_tag:\"TemplateLiteral\",parts:h.parts.map(T=>y(T)),checks:b,...so(h.annotations)};case\"Arrays\":return{_tag:\"Arrays\",elements:h.elements.map(T=>{let C=bu(T),L=C.context?.annotations;return{isOptional:Qt(C),type:y(T),...so(L)}}),rest:h.rest.map(T=>y(T)),checks:b,...so(h.annotations)};case\"Objects\":return{_tag:\"Objects\",propertySignatures:h.propertySignatures.map(T=>{let C=bu(T.type),L=C.context?.annotations;return{name:T.name,type:y(T.type),isOptional:Qt(C),isMutable:fF(C),...so(L)}}),indexSignatures:h.indexSignatures.map(T=>({parameter:y(T.parameter),type:y(T.type)})),checks:b,...so(h.annotations)};case\"Union\":return{_tag:\"Union\",types:h.types.map(T=>y(T)),...h.options===void 0?{}:{options:h.options},checks:b,...so(h.annotations)};case\"Suspend\":return{_tag:\"Suspend\",checks:[],thunk:y(h.thunk()),...so(h.annotations)}}}function w(h){return h?.map(E)??[]}function E(h){switch(h._tag){case\"Filter\":return{_tag:\"Filter\",aborted:h.aborted,..._(h.annotations)};case\"FilterGroup\":return{_tag:\"FilterGroup\",checks:wn(h.checks,E),..._(h.annotations)}}}function U(h){if(h===void 0)return;let{representation:b,...T}=h;return{...b===void 0?void 0:{representation:b},...Object.keys(T).length===0?void 0:{annotations:T}}}function _(h){if(h===void 0)return;let{representation:b,...T}=h,C=b===void 0?void 0:b.schemas===void 0?b:{...b,schemas:b.schemas.map(L=>y(yt(L)))};return{...C===void 0?void 0:{representation:C},...Object.keys(T).length===0?void 0:{annotations:T}}}}function hN(e){return Ve(e)?e.value:{issues:[{message:cb(e.cause)}]}}function gN(e,t){let r=nt(e),n={errors:\"all\",...t?.parseOptions},o=OP(t),s=a=>{let i=new Qa(\"sync\"),c=Nl(Zb(r(a,n),{onFailure:o,onSuccess:f=>({value:f})}),{scheduler:i});c.currentDispatcher?.flush();let u=c.pollUnsafe();return u?hN(u):new Promise(f=>{c.addObserver(p=>{f(hN(p))})})};if(Object.hasOwn(e,\"~standard\")){let a=e;return\"validate\"in a[\"~standard\"]||Object.assign(a[\"~standard\"],{validate:s}),a}return Object.assign(e,{\"~standard\":{version:1,vendor:\"effect\",validate:s}})}function _G(e){let t=mp(Cu(e));return ix(t)}function xN(e,t){let r=_G(e);if(t===\"draft-2020-12\"){let n=r.schema;return Object.keys(r.definitions).length>0&&(n.$defs=r.definitions),n}else if(t===\"draft-07\"){let n=GF(r),o=n.schema;return Object.keys(n.definitions).length>0&&(o.definitions=n.definitions),o}throw new globalThis.Error(`Unsupported target: ${t}`)}function yN(e){let t={input(r){return xN(e.ast,r.target)},output(r){return xN(yt(e.ast),r.target)}};if(Object.hasOwn(e,\"~standard\")){let r=e;return\"jsonSchema\"in r[\"~standard\"]||Object.assign(r[\"~standard\"],{jsonSchema:t}),r}return Object.assign(e,{\"~standard\":{version:1,vendor:\"effect\",jsonSchema:t}})}function bN(e,t){let r=[];return ST(e,t,\"\",r),r}function ST(e,t,r,n){if(!Object.is(e,t)){if(Array.isArray(e)&&Array.isArray(t)){let o=e.length,s=t.length,a=Math.min(o,s);for(let i=0;i=s;i--)n.push({op:\"remove\",path:`${r}/${i}`});for(let i=o;ii)throw new Error(`Array index out of bounds at \"${t.path}\".`);let c=o.slice();return t.op===\"add\"?c.splice(a,0,t.value):t.op===\"remove\"?c.splice(a,1):c[a]=t.value,SN(s,c)}if(cx(o)){if(t.op!==\"add\"&&!Object.hasOwn(o,n))throw new Error(`Property \"${n}\" does not exist at \"${t.path}\".`);let a={...o};return t.op===\"remove\"?delete a[n]:M(a,n,t.value),SN(s,a)}throw new Error(`Cannot ${t.op} at \"${t.path}\" (parent not found or not a container).`)}function FG(e,t){let r=PG(t);if(r.length===0)return null;let n=r[r.length-1],o=[],s=e;for(let a=0;a=s.length)return null;o.push({container:s,token:c}),s=s[c];continue}if(cx(s)){if(!Object.hasOwn(s,i))return null;o.push({container:s,token:i}),s=s[i];continue}return null}return{stack:o,parent:s,lastToken:n}}function SN(e,t){let r=t;for(let n=e.length-1;n>=0;n--){let{container:o,token:s}=e[n];if(Array.isArray(o)){let a=o.slice();a[s]=r,r=a}else{let a={...o};M(a,s,r),r=a}}return r}function wN(e){let t=rx(e),r=Xh(t),n=Qh(t);return{empty:[],diff:(o,s)=>bN(r(o),r(s)),combine:(o,s)=>[...o,...s],patch:(o,s)=>{let a=r(o),i=EN(s,a);return Object.is(i,a)?o:n(i)}}}function CN(e,t){let r=iu(e.ast)??eI(e.ast),n=MF(ox(e));return o=>n(o).pipe(j(s=>UG(s,{rootName:r,...t})))}function UG(e,t){let r=t.rootName??\"root\",n=t.arrayItemName??\"item\",o=t.pretty??!0,s=t.indent??\" \",a=t.sortKeys??!0,i=new Set,c=[];return f(r,e,0),c.join(o?`\n`:\"\");function u(p,d){c.push(o?s.repeat(p)+d:d)}function f(p,d,m,x){let{attrs:y,safe:S}=Au.tagInfo(p,x);if(d===void 0)u(m,`<${S}${y}/>`);else if(typeof d==\"string\")u(m,`<${S}${y}>${Au.escapeText(d)}`);else if(typeof d!=\"object\"||d===null)u(m,`<${S}${y}>${Au.escapeText(F(d))}`);else{if(i.has(d))throw new globalThis.Error(\"Cycle detected while serializing to XML.\",{cause:d});i.add(d);try{if(globalThis.Array.isArray(d)){if(d.length===0){u(m,`<${S}${y}/>`);return}u(m,`<${S}${y}>`);for(let U of d)f(n,U,m+1);u(m,``);return}let w=d,E=Object.keys(w);if(a&&E.sort(),E.length===0){u(m,`<${S}${y}/>`);return}u(m,`<${S}${y}>`);for(let U of E)f(Au.parseTagName(U).safe,w[U],m+1,U);u(m,``)}finally{i.delete(d)}}}}var Au={escapeText(e){return e.replace(/&/g,\"&\").replace(//g,\">\")},escapeAttribute(e){return e.replace(/&/g,\"&\").replace(/\"/g,\""\").replace(//g,\">\")},parseTagName(e){let t=e,r=e;return/^[A-Za-z_]/.test(r)||(r=\"_\"+r),r=r.replace(/[^A-Za-z0-9._-]/g,\"_\"),/^xml/i.test(r)&&(r=\"_\"+r),{safe:r,changed:r!==t}},tagInfo(e,t){let{changed:r,safe:n}=Au.parseTagName(e),s=r||t&&t!==e?` data-name=\"${Au.escapeAttribute(t??e)}\"`:\"\";return{safe:n,attrs:s}}};var bT=Vr(e=>Sa(e));function Sa(e){let t=As(e)?.toEquivalence;if(t)return t(gu(e)?e.typeParameters.map(Sa):[]);switch(e._tag){case\"Never\":return Ja();case\"Declaration\":return LG(e);case\"Null\":case\"Undefined\":case\"Void\":case\"Unknown\":case\"Any\":case\"String\":case\"Number\":case\"Boolean\":case\"BigInt\":case\"Symbol\":case\"Literal\":case\"UniqueSymbol\":case\"ObjectKeyword\":case\"Enum\":case\"TemplateLiteral\":return J;case\"Arrays\":{let r=e.elements.map(Sa),n=e.rest.map(Sa),[o,...s]=n,a=s.length;return nr((i,c)=>{if(!Array.isArray(i)||!Array.isArray(c))return!1;let u=i.length;if(u!==c.length)return!1;let f=0;for(;f0){for(;fSa(o.type)),n=e.indexSignatures.map(o=>Sa(o.type));return nr((o,s)=>{if(!fr(o)||!fr(s))return!1;for(let a=0;a[Ui(s),Sa(e.types[a])]);return nr((s,a)=>{let i=n(s,!1);for(let c=0;c(r??=bT(e.thunk()))(n,o))}}}function LG(e){let t=e.annotations?.representation;if(t===void 0)return J;switch(t.id){case\"effect/schema/Option\":{let[r]=$i(e);return(n,o)=>n._tag===o._tag&&(n._tag===\"None\"||r(n.value,o.value))}case\"effect/schema/Result\":{let[r,n]=$i(e);return(o,s)=>o._tag===s._tag&&(o._tag===\"Success\"?r(o.success,s.success):n(o.failure,s.failure))}case\"effect/schema/CauseReason\":{let[r,n]=$i(e);return kN(r,n)}case\"effect/schema/Cause\":{let[r,n]=$i(e);return AN(r,n)}case\"effect/schema/Exit\":{let[r,n,o]=$i(e),s=AN(n,o);return(a,i)=>a._tag===i._tag&&(a._tag===\"Success\"?r(a.value,i.value):s(a.cause,i.cause))}case\"effect/schema/ReadonlyMap\":{let[r,n]=$i(e);return ld(r,n)}case\"effect/schema/ReadonlySet\":return wf($i(e)[0]);case\"effect/schema/RegExp\":return(r,n)=>r.source===n.source&&r.flags===n.flags;case\"effect/schema/URL\":return(r,n)=>r.toString()===n.toString();default:return J}}function $i(e){return e.typeParameters.map(Sa)}function kN(e,t){return(r,n)=>{if(r._tag!==n._tag)return!1;switch(r._tag){case\"Fail\":return e(r.error,n.error);case\"Die\":return t(r.defect,n.defect);case\"Interrupt\":return r.fiberId===n.fiberId}}}function AN(e,t){let r=Sd(kN(e,t));return(n,o)=>r(n.reasons,o.reasons)}function ON(e,t){return r=>{switch(r._tag){case\"Fail\":return`Fail(${e(r.error)})`;case\"Die\":return`Die(${t(r.defect)})`;case\"Interrupt\":return\"Interrupt\"}}}function vN(e,t){let r=ON(e,t);return n=>`Cause([${n.reasons.map(r).join(\", \")}])`}function _N(e,t){let r=new Map;return n(e);function n(a){let i=r.get(a);return i===void 0&&(i=o(a),r.set(a,i)),i}function o(a){let i=As(a)?.toFormatter;if(typeof i==\"function\")return i(gu(a)?a.typeParameters.map(n):[]);if(t?.onBefore){let c=t.onBefore(a,n);if(c!==void 0)return c}return s(a)}function s(a){switch(a._tag){default:return F;case\"Never\":return()=>\"never\";case\"Void\":return()=>\"void\";case\"Declaration\":{let i=a.annotations?.representation;if(i===void 0)return F;let c=a.typeParameters.map(n);switch(i.id){case\"effect/schema/Option\":{let[u]=c;return Wt({onNone:()=>\"none()\",onSome:f=>`some(${u(f)})`})}case\"effect/schema/Result\":{let[u,f]=c;return wr({onSuccess:p=>`success(${u(p)})`,onFailure:p=>`failure(${f(p)})`})}case\"effect/schema/CauseReason\":return ON(c[0],c[1]);case\"effect/schema/Cause\":return vN(c[0],c[1]);case\"effect/schema/Exit\":{let[u,f,p]=c,d=vN(f,p);return m=>{switch(m._tag){case\"Success\":return`Exit.Success(${u(m.value)})`;case\"Failure\":return`Exit.Failure(${d(m.cause)})`}}}case\"effect/schema/ReadonlyMap\":{let[u,f]=c;return p=>{let d=p.size;if(d===0)return\"ReadonlyMap(0) {}\";let m=globalThis.Array.from(p.entries()).sort().map(([x,y])=>`${u(x)} => ${f(y)}`);return`ReadonlyMap(${d}) { ${m.join(\", \")} }`}}case\"effect/schema/HashMap\":{let[u,f]=c;return p=>{let d=VR(p);if(d===0)return\"HashMap(0) {}\";let m=zl(p).sort().map(([x,y])=>`${u(x)} => ${f(y)}`);return`HashMap(${d}) { ${m.join(\", \")} }`}}case\"effect/schema/ReadonlySet\":{let[u]=c;return f=>{let p=f.size;if(p===0)return\"ReadonlySet(0) {}\";let d=globalThis.Array.from(f.values()).sort().map(m=>`${u(m)}`);return`ReadonlySet(${p}) { ${d.join(\", \")} }`}}case\"effect/schema/HashSet\":{let[u]=c;return f=>{let p=eP(f);if(p===0)return\"HashSet(0) {}\";let d=globalThis.Array.from(f).sort().map(m=>`${u(m)}`);return`HashSet(${p}) { ${d.join(\", \")} }`}}case\"effect/schema/Chunk\":{let[u]=c;return f=>{let p=IR(f);if(p===0)return\"Chunk(0) {}\";let d=globalThis.Array.from(f).sort().map(m=>`${u(m)}`);return`Chunk(${p}) { ${d.join(\", \")} }`}}default:return F}}case\"Arrays\":{let i=a.elements.map(d=>n(d)),c=a.rest.map(n),[u,...f]=c,p=f.length;return d=>{let m=[],x=0;for(;x0){for(;xn(u.type)),c=a.indexSignatures.map(u=>n(u.type));return a.propertySignatures.length===0&&a.indexSignatures.length===0?F:u=>{let f=[],p=new Set;for(let d=0;d0?\"{ \"+f.join(\", \")+\" }\":\"{}\"}}case\"Union\":{let i=yt(a).types,c=sp(i),u=i.map((f,p)=>[Ui(f),n(a.types[p])]);return f=>{let p=c(f,!1);for(let d=0;d(i??=n(a.thunk()))(c)}}}}var ET=l(2,(e,t)=>wT(e,(r,n)=>PN(t,r)?[r,n]:void 0)),IT=l(2,(e,t)=>wT(e,(r,n)=>PN(t,r)?void 0:[r,n])),RN=l(2,(e,t)=>({...e,...t}));var TT=l(2,(e,t)=>wT(e,(r,n)=>[Object.hasOwn(t,r)?t[r]:r,n]));var mr=e=>e;function PN(e,t){return e.some(r=>r===t||typeof r==\"number\"&&String(r)===t)}function wT(e,t){let r={};for(let n of Reflect.ownKeys(e)){if(!Object.prototype.propertyIsEnumerable.call(e,n))continue;let o=t(n,e[n]);if(o){let[s,a]=o;M(r,s,a)}}return r}function NN(e,t){return ao(ku(\"Iso\",e,t))}function MN(e,t){return ao(ku(\"Lens\",e,t))}function ku(e,t,r){return[{_tag:\"PrimitiveNode\",kind:e,get:t,set:r}]}var jG={kind:\"Iso\",get:R,set:R},ux=class{_tag=\"PathNode\";kind=\"Lens\";path;get;set;constructor(t){this.path=t,this.get=r=>{let n=r;for(let o=0;o{let o=fx(n),s=o,a=0;for(;agF(t,r)}};function Bi(e,t){if(e.length===0)return t;if(t.length===0)return e;let r=e.slice();for(let n=0;nr)}modify(t){return r=>Lf(Lt(this.getResult(r),n=>this.replaceResult(t(n),r)),()=>r)}compose(t){return ao(Bi(this.node,t.node))}key(t){return ao(Bi(this.node,[new ux([t])]))}optionalKey(t){return ao(Bi(this.node,ku(\"Lens\",r=>r[t],(r,n)=>{let o=fx(n);return r===void 0?Array.isArray(o)&&(typeof t==\"number\"||typeof t==\"string\"&&Cc(t))?o.splice(Number(t),1):delete o[t]:M(o,t,r),o})))}check(...t){return ao(Bi(this.node,[new hp(t)]))}refine(t,r){return ao(Bi(this.node,[new hp([jh(t,r)])]))}tag(t){let r=W(new de({expected:`${JSON.stringify(t)} tag`}));return ao(Bi(this.node,ku(\"Prism\",n=>n._tag===t?B(n):r,R)))}at(t,...r){let n=W(new ut([t],new ma(void 0)));return ao(Bi(this.node,ku(\"Optional\",o=>Object.hasOwn(o,t)?B(o[t]):n,(o,s)=>{if(Object.hasOwn(s,t)){let a=fx(s);return M(a,t,o),B(a)}else return n})))}pick(t){return this.compose(MN(ET(t),(r,n)=>({...IT(n,t),...r})))}omit(t){return this.compose(MN(IT(t),(r,n)=>({...ET(n,t),...r})))}notUndefined(){return this.refine(Io,{expected:\"a value other than `undefined`\"})}forEach(t){let r=t(lx());return zG(n=>Tr(this.getResult(n),o=>{let s=[];for(let a=0;aLt(this.getResult(o),s=>{let a=[];for(let c=0;cLf(Lt(this.getResult(r),n=>this.replaceResult(n.map(t),r)),()=>r)}},AT=class extends vu{get;set;constructor(t,r,n){super(t,o=>B(r(o)),o=>B(n(o))),this.get=r,this.set=n}replace(t,r){return this.set(t)}modify(t){return r=>this.set(t(this.get(r)))}},kT=class extends vu{get;constructor(t,r,n){super(t,o=>B(r(o)),(o,s)=>B(n(o,s))),this.get=r,this.replace=n}modify(t){return r=>this.replace(t(this.get(r)),r)}},vT=class extends vu{set;constructor(t,r,n){super(t,r,(o,s)=>B(n(o))),this.set=n}replace(t,r){return this.set(t)}modify(t){return r=>Lf(Tr(this.getResult(r),n=>this.set(t(n))),()=>r)}};function ao(e){let t=e[0]??jG;if(e.length>1){let r=e.reduce((n,o)=>WG(n,o.kind),\"Iso\");t={kind:r,get:qG(e,r),set:HG(e,r)}}switch(t.kind){case\"Iso\":return new AT(e,t.get,t.set);case\"Lens\":return new kT(e,t.get,t.set);case\"Prism\":return new vT(e,t.get,t.set);case\"Optional\":return new vu(e,t.get,t.set)}}function fx(e){if(Array.isArray(e))return e.slice();if(typeof e==\"object\"&&e!==null){let t=Object.getPrototypeOf(e);if(t!==Object.prototype&&t!==null)throw new Error(\"Cannot clone object with non-Object constructor or null prototype\");return{...e}}return e}function qG(e,t){return r=>{for(let n=0;n{for(let n=e.length-1;n>=0;n--)r=e[n].set(r);return r}:(r,n)=>{let o=e.length,s=new Array(o);for(let a=0;a=0;a--){let i=e[a];if(FN(i.kind))r=i.set(r);else if(i.kind===\"Lens\")r=i.set(r,s[a]);else{let c=i.set(r,s[a]);if(ce(c))return c;r=c.success}}return t===\"Optional\"?B(r):r}}function OT(e){return e===\"Prism\"||e===\"Optional\"}function FN(e){return e===\"Iso\"||e===\"Prism\"}function WG(e,t){return e===\"Iso\"?t:t===\"Iso\"||e===t?e:\"Optional\"}var JG=ao([]);function lx(){return JG}function DN(e){let t=nx(e);return NN(Xh(t),Qh(t))}function LN(e){return lx()}function $N(e){return lx()}var Ou=\"~effect/Schema/SchemaError\";function BN(e){return k(e,Ou)&&e[Ou]===Ou}var yn=\"~effect/net/NetAddress\";var io=e=>e.value,co=e=>e,vs=e=>e.bytes,ji=class extends Fr(\"NetAddressError\"){},_u=e=>k(e,yn),hr=e=>_u(e)&&e._tag===\"Ipv4Address\",Yo=e=>_u(e)&&e._tag===\"Ipv6Address\",jN=e=>hr(e)||Yo(e),Ru=e=>hr(e)?32:128,Pu=e=>_u(e)&&e._tag===\"MacAddress\",gp=e=>_u(e)&&e._tag===\"InetAddressV4\",yp=e=>_u(e)&&e._tag===\"InetAddressV6\",_T=e=>gp(e)||yp(e),px=e=>_u(e)&&e._tag===\"UnixPathAddress\",zN=e=>_T(e)||px(e),GG={_tag:\"Ipv4Address\",[yn]:yn,[ae](e){return hr(e)&&io(this)===io(e)},[se](){return qt(le(QG,io(this)|0))},toString(){return uo(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},VG={_tag:\"Ipv6Address\",[yn]:yn,[ae](e){if(!Yo(e))return!1;let t=co(this),r=co(e);return t.w0===r.w0&&t.w1===r.w1&&t.w2===r.w2&&t.w3===r.w3},[se](){let e=co(this),t=le(XG,e.w0|0);return t=le(t,e.w1|0),t=le(t,e.w2|0),qt(le(t,e.w3|0))},toString(){return uo(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},ZG={_tag:\"MacAddress\",[yn]:yn,[ae](e){return Pu(e)&&YG(vs(this),vs(e))},[se](){return le(e3,To(vs(this)))},toString(){return PT(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},YG=(e,t)=>{if(e.length!==t.length)return!1;for(let r=0;r{let t=Object.create(GG);return t.value=e>>>0,Object.freeze(t)},r3=(e,t,r,n)=>{let o=Object.create(VG);return o.w0=e>>>0,o.w1=t>>>0,o.w2=r>>>0,o.w3=n>>>0,Object.freeze(o)},n3=(e,t,r,n)=>(e<<24|t<<16|r<<8|n)>>>0;var xp=e=>[e>>>24,e>>>16&255,e>>>8&255,e&255],o3=e=>{let t=Object.assign(Object.create(ZG),{bytes:new Uint8Array(e)});return Object.freeze(t)};var St=(e,t)=>W(new ji({input:e,message:t})),s3=e=>e.every(t=>Number.isInteger(t)&&t>=0&&t<=255)?B(t3(n3(e[0],e[1],e[2],e[3]))):St(e,\"octets must be integers from 0 through 255\"),a3=e=>e.every(t=>Number.isInteger(t)&&t>=0&&t<=65535)?B(r3(e[0]*65536+e[1],e[2]*65536+e[3],e[4]*65536+e[5],e[6]*65536+e[7])):St(e,\"segments must be integers from 0 through 65535\");var qN=e=>/^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/.test(e)?B(o3(new Uint8Array(e.split(\":\").map(t=>Number.parseInt(t,16))))):St(e,\"expected six two-digit hexadecimal octets separated by colons\");var Mu=e=>{let t=e.split(\".\");if(t.length!==4||t.some(n=>!/^\\d{1,3}$/.test(n)))return St(e,\"expected exactly four decimal octets\");if(t.some(n=>n.length>1&&n[0]===\"0\"))return St(e,\"leading zeroes are not allowed\");let r=t.map(Number);return r.some(n=>n>255)?St(e,\"octets must be at most 255\"):s3([r[0],r[1],r[2],r[3]])},i3=e=>{if(e.includes(\"[\")||e.includes(\"]\")||e.includes(\"%\"))return St(e,\"brackets and zone identifiers are not valid in a bare IPv6 address\");let t=e.split(\"::\");if(t.length>2)return St(e,\"only one compression marker is allowed\");let r=t[0]===\"\"?[]:t[0].split(\":\"),n=t.length===2&&t[1]!==\"\"?t[1].split(\":\"):[];if(r.some(f=>f===\"\")||n.some(f=>f===\"\"))return St(e,\"empty segments are only valid in the compression marker\");let o=n.length>0?n[n.length-1]:r.length>0?r[r.length-1]:\"\",s;if(o.includes(\".\")){if(t.length===2&&n.length===0)return St(e,\"embedded IPv4 syntax must be trailing\");let f=Tr(Mu(o),p=>{let d=io(p);return[d>>>16,d&65535]});if(ce(f))return f;s=f.success,n.length>0?n.pop():r.pop()}let a=r.length+n.length+(s?2:0);if(t.length===1?a!==8:a>=8)return St(e,t.length===1?\"expected eight segments\":\"compression must replace at least one segment\");let i=f=>/^[0-9a-fA-F]{1,4}$/.test(f)?Number.parseInt(f,16):void 0,c=r.map(i),u=n.map(i);return c.some(f=>f===void 0)||u.some(f=>f===void 0)?St(e,\"segments must contain one through four hexadecimal digits\"):B([...c,...Array(8-a).fill(0),...u,...s??[]])},Sp=e=>Lt(i3(e),t=>a3(t)),bp=e=>e.includes(\":\")?Sp(e):Mu(e);var RT=e=>xp(io(e)),c3=e=>{let{w0:t,w1:r,w2:n,w3:o}=co(e);return[t>>>16,t&65535,r>>>16,r&65535,n>>>16,n&65535,o>>>16,o&65535]},HN=e=>{let{w0:t,w1:r,w2:n,w3:o}=co(e);return[...xp(t),...xp(r),...xp(n),...xp(o)]};var PT=e=>Array.from(vs(e),t=>t.toString(16).padStart(2,\"0\")).join(\":\"),dx=e=>vs(e).every(t=>t===255),u3=e=>(vs(e)[0]&1)!==0,mx=e=>(vs(e)[0]&1)===0,MT=e=>(vs(e)[0]&2)!==0,FT=e=>(vs(e)[0]&2)===0;var uo=e=>{if(hr(e))return RT(e).join(\".\");let t=c3(e);if(f3(e))return`::ffff:${t[6]>>8}.${t[6]&255}.${t[7]>>8}.${t[7]&255}`;let r=-1,n=0,o=-1;for(let i=0;i<=8;i++)i<8&&t[i]===0?o===-1&&(o=i):o!==-1&&(i-o>n&&(r=o,n=i-o),o=-1);if(n<2)return t.map(i=>i.toString(16)).join(\":\");let s=t.slice(0,r).map(i=>i.toString(16)).join(\":\"),a=t.slice(r+n).map(i=>i.toString(16)).join(\":\");return`${s}::${a}`},hx=e=>{if(hr(e))return io(e)===0;let{w0:t,w1:r,w2:n,w3:o}=co(e);return t===0&&r===0&&n===0&&o===0},NT=e=>{if(hr(e))return io(e)>>>24===127;let{w0:t,w1:r,w2:n,w3:o}=co(e);return t===0&&r===0&&n===0&&o===1},Fu=e=>Pu(e)?u3(e):hr(e)?io(e)>>>28===14:co(e).w0>>>24===255,xx=e=>Pu(e)?dx(e):io(e)===4294967295,UT=e=>Pu(e)?mx(e):!Fu(e)&&!hx(e)&&(!hr(e)||!xx(e)),DT=e=>hr(e)?io(e)>>>16===43518:(co(e).w0>>>16&65472)===65152,LT=e=>{let t=io(e);return t>>>24===10||(t>>>16&65520)===44048||t>>>16===49320},$T=e=>(co(e).w0>>>24&254)===252,f3=e=>{let{w0:t,w1:r,w2:n}=co(e);return t===0&&r===0&&n===65535};var l3={_tag:\"InetAddressV4\",[yn]:yn,[ae](e){return gp(e)&&this.port===e.port&&J(this.address,e.address)},[se](){return le(H(this.address),It(this.port))},toString(){return gx(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},p3={_tag:\"InetAddressV6\",[yn]:yn,[ae](e){return yp(e)&&this.port===e.port&&this.scopeId===e.scopeId&&J(this.address,e.address)},[se](){return le(le(H(this.address),It(this.port)),It(this.scopeId))},toString(){return gx(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},WN=e=>Number.isInteger(e)&&e>=0&&e<=65535,d3=(e,t)=>{if(!WN(t))return St(e,\"port must be an integer from 0 through 65535\");let r=Object.create(l3);return r.address=e,r.port=t,B(Object.freeze(r))},m3=(e,t,r)=>{if(!WN(t))return St(e,\"port must be an integer from 0 through 65535\");let n=r?.scopeId??0;if(!Number.isInteger(n)||n<0||n>4294967295)return St(e,\"scopeId must be an unsigned 32-bit integer\");let o=Object.create(p3);return o.address=e,o.port=t,o.scopeId=n,B(Object.freeze(o))};var JN=e=>{let t,r,n=0,o=e.startsWith(\"[\");if(o){let s=e.indexOf(\"]\");if(s<0||e[s+1]!==\":\"||e.indexOf(\"]\",s+1)!==-1)return St(e,\"expected [IPv6]:port\");t=e.slice(1,s),r=e.slice(s+2);let a=t.indexOf(\"%\");if(a!==-1){let i=t.slice(a+1);if(t.indexOf(\"%\",a+1)!==-1||!/^\\d+$/.test(i))return St(e,\"scope identifier must be decimal\");if(n=Number(i),!Number.isInteger(n)||n>4294967295)return St(e,\"scope identifier must be an unsigned 32-bit integer\");t=t.slice(0,a)}}else{let s=e.lastIndexOf(\":\");if(s<0)return St(e,\"expected host:port or [IPv6]:port\");if(e.indexOf(\":\")!==s)return St(e,\"IPv6 addresses must be bracketed\");t=e.slice(0,s),r=e.slice(s+1)}return/^(0|[1-9]\\d*)$/.test(r)?Lt(bp(t),s=>o!==Yo(s)?St(e,\"only IPv6 addresses use brackets\"):Yo(s)?m3(s,Number(r),{scopeId:n}):d3(s,Number(r))):St(e,\"port must be an unpadded decimal integer\")};var h3=e=>uo(e.address)+(yp(e)&&e.scopeId!==0?`%${e.scopeId}`:\"\");var gx=e=>{let t=h3(e);return gp(e)?`${t}:${e.port}`:`[${t}]:${e.port}`};var x3={_tag:\"UnixPathAddress\",[yn]:yn,[ae](e){return px(e)&&this.path===e.path},[se](){return le(Re(\"UnixPathAddress\"),Re(this.path))},toString(){return this.path},toJSON(){return this.toString()},[Te](){return this.toJSON()}},KN=e=>{let t=Object.create(x3);return t.path=e,Object.freeze(t)};var jT=\"~effect/net/IpInterface\",GN=e=>Ep(e)&&hr(e.address),VN=e=>Ep(e)&&Yo(e.address),Ep=e=>k(e,jT),g3={_tag:\"IpInterface\",[jT]:jT,[ae](e){return Ep(e)&&this.prefixLength===e.prefixLength&&J(this.address,e.address)},[se](){return le(H(this.address),It(this.prefixLength))},toString(){return Tp(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},zT=(e,t)=>W(new ji({input:e,message:t})),Ip=(e,t)=>{let r=Ru(e);if(!Number.isInteger(t)||t<0||t>r)return zT(e,`prefix length must be an integer from 0 through ${r}`);let n=Object.assign(Object.create(g3),{address:e,prefixLength:t});return B(Object.freeze(n))},qT=(e,t)=>{let r=e.indexOf(\"/\");if(r===-1&&t?.prefix!==\"required\")return B({address:e,prefixLength:void 0});if(r<=0||r!==e.lastIndexOf(\"/\")||r===e.length-1)return zT(e,\"expected an address and prefix length separated by one slash\");let n=e.slice(r+1);return/^(0|[1-9][0-9]*)$/.test(n)?B({address:e.slice(0,r),prefixLength:Number(n)}):zT(e,\"prefix length must be an unpadded ASCII decimal integer\")},yx=(e,t)=>Lt(qT(e,t),r=>Lt(Mu(r.address),n=>Ip(n,r.prefixLength??Ru(n)))),Sx=(e,t)=>Lt(qT(e,t),r=>Lt(Sp(r.address),n=>Ip(n,r.prefixLength??Ru(n)))),bx=(e,t)=>Lt(qT(e,t),r=>Lt(bp(r.address),n=>Ip(n,r.prefixLength??Ru(n))));var Tp=e=>`${uo(e.address)}/${e.prefixLength}`;var HT=\"~effect/net/IpNetwork\",YN=e=>wp(e)&&hr(e.address),QN=e=>wp(e)&&Yo(e.address),wp=e=>k(e,HT),y3={_tag:\"IpNetwork\",[HT]:HT,[ae](e){return wp(e)&&this.prefixLength===e.prefixLength&&J(this.address,e.address)},[se](){return le(H(this.address),It(this.prefixLength))},toString(){return Cp(this)},toJSON(){return this.toString()},[Te](){return this.toJSON()}},S3=e=>{let t=Object.assign(Object.create(y3),{address:e.address,prefixLength:e.prefixLength});return Object.freeze(t)},b3=e=>hr(e)?RT(e):HN(e);var E3=(e,t)=>{let r=Math.floor(t/8),n=t%8;return e.map((o,s)=>sLt(Ip(e,t),r=>{let n=b3(e),o=E3(n,t);return n.some((s,a)=>s!==o[a])?W(new ji({input:e,message:\"address has non-zero host bits\"})):B(S3(r))});var XN=e=>Lt(yx(e,{prefix:\"required\"}),t=>WT(t.address,t.prefixLength)),eU=e=>Lt(Sx(e,{prefix:\"required\"}),t=>WT(t.address,t.prefixLength)),tU=e=>Lt(bx(e,{prefix:\"required\"}),t=>WT(t.address,t.prefixLength));var Cp=e=>`${uo(e.address)}/${e.prefixLength}`;var Ap=tx;function qr(){return(e,t,r)=>q(new yu(e.map(op),n=>t(n.map(o=>q(o))),r))}function Be(e,t){return qr()([],()=>(r,n,o)=>e(r)?g(r):P(new ft(n,r,o)),t)}function T3(e){return e}function w3(e){return t=>t.annotate(e)}function C3(e){return t=>JT(JT(t).annotate(e))}function A3(e){return t=>t.rebuild(cp(t.ast,e))}function k3(e){return e}var Os=class extends Fr(\"SchemaError\"){[Ou]=Ou;constructor(t){let r=Co();Yr(0);try{super({issue:t})}finally{Yr(r)}}get message(){return _P(this.issue)}toString(){return`SchemaError(${this.message})`}};function hU(e){return BN(e)}function xU(e){return Ne(e)?YT(e):rt(e,t=>Is(()=>Ss(t,r=>new Os(r))))}function YT(e){return Ve(e)?e:sn(Ss(e.cause,t=>new Os(t)))}function gU(e,t){let r;for(let n of e.reasons){if(!na(n)||!hU(n.error))throw new globalThis.Error(t,{cause:e});r??=n.error}if(r===void 0)throw new globalThis.Error(t,{cause:e});return r}function yU(e){return ah(e).then(t=>{if(Ve(t))return t.value;throw gU(t.cause,\"Promise adapter can only reject schema errors\")})}function SU(e){let t=Wo(e);if(Ve(t))return t.value;throw gU(t.cause,\"Sync adapter can only throw schema errors\")}function v3(e,t){return gN(e,t)}function O3(e){return yN(e)}var _3=rT,R3=OF;function Ex(e,t){let r=nt(e,t);return(n,o)=>xU(r(n,o))}var P3=Ex;function bU(e,t){let r=_F(e,t);return(n,o)=>YT(r(n,o))}var M3=bU,F3=oT,N3=RF;function EU(e,t){let r=PF(e,t);return(n,o)=>Dd(r(n,o),s=>new Os(s))}var U3=EU;function IU(e,t){let r=Ex(e,t);return(n,o)=>yU(r(n,o))}var D3=IU;function TU(e,t){let r=Ex(e,t);return(n,o)=>SU(r(n,o))}var L3=TU;function Ix(e,t){let r=Di(e,t);return(n,o)=>xU(r(n,o))}var $3=Ix;function wU(e,t){let r=FF(e,t);return(n,o)=>YT(r(n,o))}var B3=wU,j3=sT,z3=NF;function CU(e,t){let r=UF(e,t);return(n,o)=>Dd(r(n,o),s=>new Os(s))}var q3=CU;function AU(e,t){let r=Ix(e,t);return(n,o)=>yU(r(n,o))}var H3=AU;function kU(e,t){let r=Ix(e,t);return(n,o)=>SU(r(n,o))}var W3=kU,q=oo;function Tx(e){return k(e,Ap)&&e[Ap]===Ap}var qi=mr(e=>q(Ni(e.ast),{schema:e})),J3=mr(e=>e.schema),Lu=mr(e=>{let t=Ax(e);return q(iF(e.ast),{schema:t})}),K3=mr(e=>e.schema.members[0]),G3=mr(e=>q(qI(e.ast),{schema:e})),V3=mr(e=>e.schema),Hr=mr(e=>q(yt(e.ast),{schema:e})),$u=mr(e=>q(hn(e.ast),{schema:e})),kp=\"~effect/Schema/flip\";function Z3(e){return k(e,kp)&&e[kp]===kp}function JT(e){return Z3(e)?e.schema.rebuild(mn(e.ast)):q(mn(e.ast),{[kp]:kp,schema:e})}function Ct(e){let t=q(new dn(e),{literal:e,transform(r){return t.pipe(G(Ct(r),{decode:oe(()=>r),encode:oe(()=>e)}))}});return t}function vU(e){return e.map(t=>Tx(t)?t.ast:new dn(t))}function Y3(e){return q(new _I(vU(e)),{parts:e})}function Q3(e){return q(BM(vU(e)),{parts:e})}function X3(e){return q(new $M(Object.keys(e).filter(t=>typeof e[e[t]]!=\"number\").map(t=>[t,e[t]])),{enums:e})}var eV=q(OI),tV=q(DM),vp=q(eo),wx=q(kI),QT=q(vI),K=q(pn),rV=K,Wi=q(RI),XT=q(qM),OU=q(HM),Bu=q(WM),nV=q(UM),oV=q(LM);function sV(e){return q(new zM(e))}function ew(e,t){return q(e,{fields:t,mapFields(r,n){let o=r(this.fields);return ew(Lh(o,n?.unsafePreserveChecks?this.ast.checks:void 0),o)}})}function Me(e){return ew(Lh(e,void 0),e)}function aV(e){return mr(t=>t.mapFields(RN(e)))}var iV=e=>typeof e==\"symbol\"?e:globalThis.String(e);function cV(e){return function(t){let r={},n=Object.create(null),o=Object.create(null),s=new Set;for(let a of Reflect.ownKeys(t.fields)){let i=$u(t.fields[a]),c=Object.hasOwn(e,a),u=c?e[a]:a,f=iV(u);if(s.has(f))throw new globalThis.Error(`Duplicate encoded keys: ${wo(u)}`);s.add(f),M(r,u,i),c&&(n[a]=u,o[u]=a)}return Me(r).pipe(G(t,Pe({decode:TT(o),encode:TT(n)})))}}function uV(e,t){return r=>{let n=Ya(r.fields,Hr),o=Me({...n,...e});return r.pipe(G(o,Pe({decode:s=>{let a={...s};for(let i in e){let c=t[i],u=c(s);be(u)&&M(a,i,u.value)}return a},encode:s=>{let a={...s};for(let i in e)delete a[i];return a}})))}}function _s(e,t){return q(uF(e.ast,t.ast),{key:e,value:t})}function fV(e,t){return q(ZM(e.ast,t.map(op)),{schema:e,records:t})}function _U(e,t){return q(e,{elements:t,mapElements(r,n){let o=r(this.elements);return _U(NI(o,n?.unsafePreserveChecks?this.ast.checks:void 0),o)}})}function Qo(e){return _U(NI(e),e)}function lV(e,t){return q(YM(e.ast,t.map(op)),{schema:e,rest:t})}var $e=mr(e=>q(new Pn(!1,[],[e.ast]),{value:e}));function Cx(e,t,r){return t!==void 0&&r!==void 0?e.check(iw(t,r)):t!==void 0?e.check(ZU(t)):r!==void 0?e.check(XU(r)):e}var tw=mr(e=>q(new Pn(!1,[e.ast],[e.ast]),{value:e}));function pV(e){let t=$e(e),r=$e(Hr(e)),n=G(Qo([vp]),Pe({decode:o=>[o],encode:([o])=>o}))(e);return q(it([n,t]).pipe(G(r)).ast,{from:it([e,t]),to:r})}function dV(e){return $e(e).check(uw())}var mV=mr(e=>q(QM(e.ast),{schema:e}));function RU(e,t){return q(e,{members:t,mapMembers(r,n){let o=r(this.members);return RU($h(o,this.ast.options,n?.unsafePreserveChecks?this.ast.checks:void 0),o)}})}function it(e,t){return RU($h(e,t,void 0),e)}function fo(e){let t=e.map(Ct);return q($h(t,void 0,void 0),{literals:e,members:t,mapMembers(r){return it(r(this.members))},pick(r){return fo(r)},transform(r){return it(t.map((n,o)=>n.transform(r[o])))}})}var rw=mr(e=>it([e,wx])),Ax=mr(e=>it([e,QT])),PU=mr(e=>it([e,wx,QT]));function MU(e){return q(new DI(()=>e().ast))}function hV(...e){return t=>t.check(...e)}function FU(e,t){return r=>q(Vo(r.ast,[jh(e,t)]),{schema:r})}function NU(e){return t=>q(t.ast,{schema:t,identifier:e})}function xV(e,t){return r=>(t.checks?r.check(...t.checks):r).pipe(NU(e))}function UU(e){return t=>q(sF(t.ast,new _h(e,R)),{schema:t})}function DU(e){return t=>q(aF(t.ast,new _h(R,e)),{schema:t})}function gV(e){return LU(e)}function LU(e){return t=>UU(ru(e))(t)}function yV(e){return $U(e)}function $U(e){return t=>DU(ru(e))(t)}function G(e,t){return r=>q(ga(r.ast,e.ast,t?Rh(t):lu()),{from:r,to:e})}function ju(){return(e,t)=>Oe()(e,{decode:t,encode:NP})}var BU=fo([\"true\",\"yes\",\"on\",\"1\",\"y\"]),jU=fo([\"false\",\"no\",\"off\",\"0\",\"n\"]),nw=fo([...BU.literals,...jU.literals]).pipe(G(XT,Pe({decode:e=>e===\"true\"||e===\"yes\"||e===\"on\"||e===\"1\"||e===\"y\",encode:e=>e?\"true\":\"false\"})));function SV(e){return t=>G(Hr(t),e)(t)}function ow(e,t){return r=>t?G(r,t)(e):G(r)(e)}function bV(e){return t=>G(t,e)($u(t))}function zU(e){return t=>q(cF(t.ast,e),{schema:t})}function qU(e){return rt(e,t=>Is(()=>Ss(t,r=>r.issue)))}function sw(e,t){let r=t?.encodingStrategy===\"omit\"?uI():jr();return n=>qi($u(n)).pipe(G(n,{decode:fI(qU(e)),encode:r}))}function EV(e,t){return r=>Hr(r).pipe(sw(e,t),ow(qi(r)))}function HU(e,t){let r=t?.encodingStrategy===\"omit\"?uI():jr();return n=>Lu($u(n)).pipe(G(n,{decode:fI(qU(e)),encode:r}))}function IV(e,t){return r=>Hr(r).pipe(HU(e,t),ow(Lu(r)))}function Hi(e){return Ct(e).pipe(zU(g(e)))}function TV(e){return Hi(e).pipe(sw(g(e),{encodingStrategy:\"omit\"}))}function kx(e,t){return Me({_tag:Hi(e),...t})}function WU(e){return t=>{let r={},n=[],o=new Set,s={},a=f=>p=>f.includes(p[e]);return i(t),Object.assign(t,{tag:e,cases:r,discriminants:n,isAnyOf:a,guards:s,match:c,matchOrElse:u});function i(f){let p=f.ast;if(MM(p)&&\"members\"in f&&globalThis.Array.isArray(f.members)&&f.members.every(Tx))return f.members.forEach(i);let d=hu(p);if(d.length>0){let m=d.find(x=>x.key===e)?.literal;if(id(m)){let x=typeof m==\"number\"?globalThis.String(m):m;if(o.has(x))throw new globalThis.Error(`Duplicate discriminant: ${globalThis.String(m)}`);o.add(x),n.push(m),M(r,m,f),M(s,m,rT(Hr(f)));return}}throw new globalThis.Error(\"No literal or unique symbol found\")}function c(){if(arguments.length===1){let x=arguments[0];return function(y){let S=y[e];return(Object.hasOwn(x,S)?x[S]:void 0)(y)}}let f=arguments[0],p=arguments[1],d=f[e];return(Object.hasOwn(p,d)?p[d]:void 0)(f)}function u(){if(arguments.length===2){let y=arguments[0],S=arguments[1];return function(w){let E=w[e];return(Object.hasOwn(y,E)?y[E]??S:S)(w)}}let f=arguments[0],p=arguments[1],d=arguments[2],m=f[e];return(Object.hasOwn(p,m)?p[m]??d:d)(f)}}}function wV(e){let t={},r=[];for(let u of Object.keys(e)){let f=kx(u,e[u]);M(t,u,f),r.push(f)}let n=it(r),{tag:o,guards:s,isAnyOf:a,match:i,matchOrElse:c}=WU(\"_tag\")(n);return q(n.ast,{tag:o,cases:t,isAnyOf:a,guards:s,match:i,matchOrElse:c})}function CV(){return e=>e}function ba(e,t){return Be(r=>r instanceof e,t)}function Oe(){return(e,t)=>new at(e.ast,Rh(t))}var Ue=Bh;function AV(e,t=void 0){return new Ri(e,t)}var rU=\"^\\\\S[\\\\s\\\\S]*\\\\S$|^\\\\S$|^$\";function JU(e){let t=new globalThis.RegExp(rU);return Ue(r=>r.trim()===r,{expected:\"a string with no leading or trailing whitespace\",representation:{id:\"effect/schema/isTrimmed\",payload:null},toJsonSchema:()=>({pattern:t.source}),toCode:()=>({runtime:\"Schema.isTrimmed()\"}),arbitraryConstraint:{patterns:[{source:rU,flags:\"\"}]},...e})}function zu(e,t){let r=e.source,n=e.flags,o=/^[dg]*uy?$/.test(n),s=n===\"\"?`new RegExp(${F(r)})`:`new RegExp(${F(r)}, ${F(n)})`;return ap(e,{toJsonSchema:()=>o?{pattern:n.endsWith(\"y\")?`^(?:${r})`:r}:[{},!0],toCode:()=>({runtime:`Schema.isPattern(${s})`}),...t})}function kV(e){return JI({toCode:()=>({runtime:\"Schema.isStringFinite()\"}),...e})}function vV(e){return KI({toCode:()=>({runtime:\"Schema.isStringBigInt()\"}),...e})}function OV(e){return VI({toCode:()=>({runtime:\"Schema.isStringSymbol()\"}),...e})}var _V=e=>e?new globalThis.RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${e}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`):/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$/;function RV(e,t){let r=_V(e);return zu(r,{expected:e?`a UUID v${e}`:\"a UUID\",representation:{id:\"effect/schema/isUUID\",payload:{version:e??null}},toJsonSchema:()=>({pattern:r.source,format:\"uuid\"}),toCode:()=>({runtime:e===void 0?\"Schema.isUUID()\":`Schema.isUUID(${e})`}),...t})}var nU=/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;function PV(e){return zu(nU,{expected:\"a GUID\",representation:{id:\"effect/schema/isGUID\",payload:null},toJsonSchema:()=>({pattern:nU.source}),toCode:()=>({runtime:\"Schema.isGUID()\"}),...e})}function MV(e){let t=/^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;return zu(t,{representation:{id:\"effect/schema/isULID\",payload:null},toJsonSchema:()=>({pattern:t.source}),toCode:()=>({runtime:\"Schema.isULID()\"}),...e})}function KU(e){let t=/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;return zu(t,{expected:\"a base64 encoded string\",representation:{id:\"effect/schema/isBase64\",payload:null},toJsonSchema:()=>({pattern:t.source}),toCode:()=>({runtime:\"Schema.isBase64()\"}),...e})}function FV(e){let t=/^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/;return zu(t,{expected:\"a base64url encoded string\",representation:{id:\"effect/schema/isBase64Url\",payload:null},toJsonSchema:()=>({pattern:t.source}),toCode:()=>({runtime:\"Schema.isBase64Url()\"}),...e})}function aw(e,t,r){if(e.length===0)return t.length===0?{}:[{},!0];let n={pattern:new globalThis.RegExp(r).source};return e===t?n:[n,!0]}function NV(e,t){let r=JSON.stringify(e),n=new globalThis.RegExp(`^${ks(e)}`),o=e.replace(/[\\uD800-\\uDBFF]$/,\"\");return Ue(s=>s.startsWith(e),{expected:`a string starting with ${r}`,representation:{id:\"effect/schema/isStartingWith\",payload:{startsWith:e}},toJsonSchema:()=>aw(o,e,`^${ks(o)}`),toCode:()=>({runtime:`Schema.isStartingWith(${F(e)})`}),arbitraryConstraint:{patterns:[{source:n.source,flags:n.flags}]},...t})}function UV(e,t){let r=JSON.stringify(e),n=new globalThis.RegExp(`${ks(e)}$`),o=e.replace(/^[\\uDC00-\\uDFFF]/,\"\");return Ue(s=>s.endsWith(e),{expected:`a string ending with ${r}`,representation:{id:\"effect/schema/isEndingWith\",payload:{endsWith:e}},toJsonSchema:()=>aw(o,e,`${ks(o)}$`),toCode:()=>({runtime:`Schema.isEndingWith(${F(e)})`}),arbitraryConstraint:{patterns:[{source:n.source,flags:n.flags}]},...t})}function DV(e,t){let r=JSON.stringify(e),n=new globalThis.RegExp(ks(e)),o=e.replace(/^[\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF]$/g,\"\");return Ue(s=>s.includes(e),{expected:`a string including ${r}`,representation:{id:\"effect/schema/isIncluding\",payload:{includes:e}},toJsonSchema:()=>aw(o,e,ks(o)),toCode:()=>({runtime:`Schema.isIncluding(${F(e)})`}),arbitraryConstraint:{patterns:[{source:n.source,flags:n.flags}]},...t})}var oU=\"^[^a-z]*$\";function LV(e){let t=new globalThis.RegExp(oU);return Ue(r=>r.toUpperCase()===r,{expected:\"a string with all characters in uppercase\",representation:{id:\"effect/schema/isUppercased\",payload:null},toJsonSchema:()=>[{pattern:t.source},!0],toCode:()=>({runtime:\"Schema.isUppercased()\"}),arbitraryConstraint:{patterns:[{source:oU,flags:\"\"}]},...e})}var sU=\"^[^A-Z]*$\";function $V(e){let t=new globalThis.RegExp(sU);return Ue(r=>r.toLowerCase()===r,{expected:\"a string with all characters in lowercase\",representation:{id:\"effect/schema/isLowercased\",payload:null},toJsonSchema:()=>[{pattern:t.source},!0],toCode:()=>({runtime:\"Schema.isLowercased()\"}),arbitraryConstraint:{patterns:[{source:sU,flags:\"\"}]},...e})}var aU=\"^(?:[^a-z][\\\\s\\\\S]*)?$\";function BV(e){let t=new globalThis.RegExp(aU);return Ue(r=>r.charAt(0).toUpperCase()===r.charAt(0),{expected:\"a string with the first character in uppercase\",representation:{id:\"effect/schema/isCapitalized\",payload:null},toJsonSchema:()=>[{pattern:t.source},!0],toCode:()=>({runtime:\"Schema.isCapitalized()\"}),arbitraryConstraint:{patterns:[{source:aU,flags:\"\"}]},...e})}var iU=\"^(?:[^A-Z][\\\\s\\\\S]*)?$\";function jV(e){let t=new globalThis.RegExp(iU);return Ue(r=>r.charAt(0).toLowerCase()===r.charAt(0),{expected:\"a string with the first character in lowercase\",representation:{id:\"effect/schema/isUncapitalized\",payload:null},toJsonSchema:()=>[{pattern:t.source},!0],toCode:()=>({runtime:\"Schema.isUncapitalized()\"}),arbitraryConstraint:{patterns:[{source:iU,flags:\"\"}]},...e})}var qu=q($I),zV=LI;function Op(e){let t=Tc(e.order),r=e.formatter??F;return(n,o)=>Ue(s=>t(s,n),{expected:`a value greater than ${r(n)}`,arbitraryConstraint:{order:e.order,minimum:n,exclusiveMinimum:!0},...e.annotate?.(n),...o})}function _p(e){let t=_d(e.order),r=e.formatter??F;return(n,o)=>Ue(s=>t(s,n),{expected:`a value greater than or equal to ${r(n)}`,arbitraryConstraint:{order:e.order,minimum:n},...e.annotate?.(n),...o})}function Rp(e){let t=vd(e.order),r=e.formatter??F;return(n,o)=>Ue(s=>t(s,n),{expected:`a value less than ${r(n)}`,arbitraryConstraint:{order:e.order,maximum:n,exclusiveMaximum:!0},...e.annotate?.(n),...o})}function Pp(e){let t=Od(e.order),r=e.formatter??F;return(n,o)=>Ue(s=>t(s,n),{expected:`a value less than or equal to ${r(n)}`,arbitraryConstraint:{order:e.order,maximum:n},...e.annotate?.(n),...o})}function Mp(e){let t=_d(e.order),r=Tc(e.order),n=Od(e.order),o=vd(e.order),s=e.formatter??F;return(a,i)=>{let c=a.exclusiveMinimum?r:t,u=a.exclusiveMaximum?o:n;return Ue(f=>c(f,a.minimum)&&u(f,a.maximum),{expected:`a value between ${s(a.minimum)}${a.exclusiveMinimum?\" (excluded)\":\"\"} and ${s(a.maximum)}${a.exclusiveMaximum?\" (excluded)\":\"\"}`,arbitraryConstraint:{order:e.order,minimum:a.minimum,maximum:a.maximum,...a.exclusiveMinimum&&{exclusiveMinimum:!0},...a.exclusiveMaximum&&{exclusiveMaximum:!0}},...e.annotate?.(a),...i})}}function qV(e){return(t,r)=>{let n=e.formatter??F;return Ue(o=>e.remainder(o,t)===e.zero,{expected:`a value that is a multiple of ${n(t)}`,...e.annotate?.(t),...r})}}function Nu(e){if(!globalThis.Number.isFinite(e))throw new globalThis.RangeError(`Expected a finite number, got ${F(e)}`);return e}var HV=Op({order:fs,annotate:e=>({representation:{id:\"effect/schema/isGreaterThan\",payload:{exclusiveMinimum:Nu(e)}},toJsonSchema:()=>({exclusiveMinimum:e}),toCode:()=>({runtime:`Schema.isGreaterThan(${F(e)})`})})}),GU=_p({order:fs,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanOrEqualTo\",payload:{minimum:Nu(e)}},toJsonSchema:()=>({minimum:e}),toCode:()=>({runtime:`Schema.isGreaterThanOrEqualTo(${F(e)})`})})}),WV=Rp({order:fs,annotate:e=>({representation:{id:\"effect/schema/isLessThan\",payload:{exclusiveMaximum:Nu(e)}},toJsonSchema:()=>({exclusiveMaximum:e}),toCode:()=>({runtime:`Schema.isLessThan(${F(e)})`})})}),JV=Pp({order:fs,annotate:e=>({representation:{id:\"effect/schema/isLessThanOrEqualTo\",payload:{maximum:Nu(e)}},toJsonSchema:()=>({maximum:e}),toCode:()=>({runtime:`Schema.isLessThanOrEqualTo(${F(e)})`})})}),Hu=Mp({order:fs,annotate:e=>{let t=e.exclusiveMinimum?!0:void 0,r=e.exclusiveMaximum?!0:void 0;return{representation:{id:\"effect/schema/isBetween\",payload:{minimum:Nu(e.minimum),maximum:Nu(e.maximum),...t&&{exclusiveMinimum:t},...r&&{exclusiveMaximum:r}}},toJsonSchema:()=>({[t?\"exclusiveMinimum\":\"minimum\"]:e.minimum,[r?\"exclusiveMaximum\":\"maximum\"]:e.maximum}),toCode:()=>({runtime:`Schema.isBetween({ minimum: ${F(e.minimum)}, maximum: ${F(e.maximum)}, exclusiveMinimum: ${F(t)}, exclusiveMaximum: ${F(r)} })`})}}});function KV(e,t){if(!globalThis.Number.isFinite(e)||e===0)throw new globalThis.RangeError(`Expected a finite non-zero number, got ${globalThis.String(e)}`);return e=globalThis.Math.abs(e),Ue(r=>I_(r,e)===0,{expected:`a value that is a multiple of ${e}`,representation:{id:\"effect/schema/isMultipleOf\",payload:{divisor:e}},toJsonSchema:()=>({multipleOf:e}),toCode:()=>({runtime:`Schema.isMultipleOf(${F(e)})`}),...t})}function vx(e){return Ue(t=>globalThis.Number.isSafeInteger(t),{expected:\"an integer\",representation:{id:\"effect/schema/isInt\",payload:null},toJsonSchema:()=>[{type:\"integer\"},!0],toCode:()=>({runtime:\"Schema.isInt()\"}),arbitraryConstraint:{number:\"integer\"},...e})}var lo=Wi.check(vx()),zi=lo.check(GU(0));function GV(e){return new Ri([vx(),Hu({minimum:-2147483648,maximum:2147483647})],{expected:\"a 32-bit integer\",...e})}function VV(e){return new Ri([vx(),Hu({minimum:0,maximum:4294967295})],{expected:\"a 32-bit unsigned integer\",...e})}function Uu(e){if(globalThis.Number.isNaN(e.getTime()))throw new globalThis.RangeError(`Expected a valid Date, got ${F(e)}`);return e.toISOString()}function Du(e){return`new Date(${F(e.getTime())})`}var ZV=Op({order:Ic,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanDate\",payload:{exclusiveMinimum:Uu(e)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isGreaterThanDate(${Du(e)})`})})}),YV=_p({order:Ic,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanOrEqualToDate\",payload:{minimum:Uu(e)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isGreaterThanOrEqualToDate(${Du(e)})`})})}),QV=Rp({order:Ic,annotate:e=>({representation:{id:\"effect/schema/isLessThanDate\",payload:{exclusiveMaximum:Uu(e)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isLessThanDate(${Du(e)})`})})}),XV=Pp({order:Ic,annotate:e=>({representation:{id:\"effect/schema/isLessThanOrEqualToDate\",payload:{maximum:Uu(e)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isLessThanOrEqualToDate(${Du(e)})`})})}),eZ=Mp({order:Ic,annotate:e=>{let t=e.exclusiveMinimum?!0:void 0,r=e.exclusiveMaximum?!0:void 0;return{representation:{id:\"effect/schema/isBetweenDate\",payload:{minimum:Uu(e.minimum),maximum:Uu(e.maximum),...t&&{exclusiveMinimum:t},...r&&{exclusiveMaximum:r}}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isBetweenDate({ minimum: ${Du(e.minimum)}, maximum: ${Du(e.maximum)}, exclusiveMinimum: ${F(t)}, exclusiveMaximum: ${F(r)} })`})}}}),tZ=Op({order:Ec,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanBigInt\",payload:{exclusiveMinimum:e.toString(10)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isGreaterThanBigInt(${F(e)})`})})}),VU=_p({order:Ec,annotate:e=>({representation:{id:\"effect/schema/isGreaterThanOrEqualToBigInt\",payload:{minimum:e.toString(10)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isGreaterThanOrEqualToBigInt(${F(e)})`})})}),rZ=Rp({order:Ec,annotate:e=>({representation:{id:\"effect/schema/isLessThanBigInt\",payload:{exclusiveMaximum:e.toString(10)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isLessThanBigInt(${F(e)})`})})}),nZ=Pp({order:Ec,annotate:e=>({representation:{id:\"effect/schema/isLessThanOrEqualToBigInt\",payload:{maximum:e.toString(10)}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isLessThanOrEqualToBigInt(${F(e)})`})})}),oZ=Mp({order:Ec,annotate:e=>{let t=e.exclusiveMinimum?!0:void 0,r=e.exclusiveMaximum?!0:void 0;return{representation:{id:\"effect/schema/isBetweenBigInt\",payload:{minimum:e.minimum.toString(10),maximum:e.maximum.toString(10),...t&&{exclusiveMinimum:t},...r&&{exclusiveMaximum:r}}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isBetweenBigInt({ minimum: ${F(e.minimum)}, maximum: ${F(e.maximum)}, exclusiveMinimum: ${F(t)}, exclusiveMaximum: ${F(r)} })`})}}});function ZU(e,t){return e=xr(e),YU(e,Math.ceil(e/2),t)}function YU(e,t,r){return Ue(n=>n.length>=e,{expected:`a value with a length of at least ${e}`,representation:{id:\"effect/schema/isMinLength\",payload:{minLength:e}},toJsonSchema:({type:n})=>n===\"string\"?e<=1?{minLength:t}:[{minLength:t},!0]:n===\"array\"?{minItems:e}:n===void 0?[{minLength:t,minItems:e},!0]:[{},!0],toCode:()=>({runtime:`Schema.isMinLength(${e})`}),[Lr]:!0,arbitraryConstraint:{minLength:e},...r})}function QU(e){return YU(1,1,e)}function XU(e,t){return e=xr(e),Ue(r=>r.length<=e,{expected:`a value with a length of at most ${e}`,representation:{id:\"effect/schema/isMaxLength\",payload:{maxLength:e}},toJsonSchema:({type:r})=>r===\"string\"?e===0?{maxLength:e}:[{maxLength:e},!0]:r===\"array\"?{maxItems:e}:r===void 0?[{maxLength:e,maxItems:e},!0]:[{},!0],toCode:()=>({runtime:`Schema.isMaxLength(${e})`}),[Lr]:!0,arbitraryConstraint:{maxLength:e},...t})}function iw(e,t,r){return e=xr(e),t=xr(t),Ue(n=>n.length>=e&&n.length<=t,{expected:e===t?`a value with a length of ${e}`:`a value with a length between ${e} and ${t}`,representation:{id:\"effect/schema/isBetweenLength\",payload:{minimum:e,maximum:t}},toJsonSchema:({type:n})=>n===\"string\"?t===0||Math.ceil(e/2)>t?{minLength:Math.ceil(e/2),maxLength:t}:[{minLength:Math.ceil(e/2),maxLength:t},!0]:n===\"array\"?{minItems:e,maxItems:t}:n===void 0?[{minLength:Math.ceil(e/2),maxLength:t,minItems:e,maxItems:t},!0]:[{},!0],toCode:()=>({runtime:`Schema.isBetweenLength(${e}, ${t})`}),[Lr]:!0,arbitraryConstraint:{minLength:e,maxLength:t},...r})}function sZ(e,t){return e=xr(e),Ue(r=>cw(r,e)>=e,{expected:`a string with at least ${e} code points`,representation:{id:\"effect/schema/isMinCodePoints\",payload:{minCodePoints:e}},toJsonSchema:()=>({minLength:e}),toCode:()=>({runtime:`Schema.isMinCodePoints(${e})`}),arbitraryConstraint:{minCodePoints:e},...t})}function aZ(e,t){return e=xr(e),Ue(r=>cw(r,e+1)<=e,{expected:`a string with at most ${e} code points`,representation:{id:\"effect/schema/isMaxCodePoints\",payload:{maxCodePoints:e}},toJsonSchema:()=>({maxLength:e}),toCode:()=>({runtime:`Schema.isMaxCodePoints(${e})`}),arbitraryConstraint:{maxCodePoints:e},...t})}function iZ(e,t,r){return e=xr(e),t=xr(t),Ue(n=>{let o=cw(n,t+1);return o>=e&&o<=t},{expected:e===t?`a string with ${e} code points`:`a string with between ${e} and ${t} code points`,representation:{id:\"effect/schema/isBetweenCodePoints\",payload:{minimum:e,maximum:t}},toJsonSchema:()=>({allOf:[{minLength:e},{maxLength:t}]}),toCode:()=>({runtime:`Schema.isBetweenCodePoints(${e}, ${t})`}),arbitraryConstraint:{minCodePoints:e,maxCodePoints:t},...r})}function cw(e,t){if(t===0)return 0;let r=0;for(let n of e)if(++r>=t)break;return r}function xr(e){if(!globalThis.Number.isFinite(e))throw new globalThis.RangeError(`Expected a finite number, got ${e}`);return Math.max(0,Math.floor(e))}function cZ(e,t){return e=xr(e),Ue(r=>r.size>=e,{expected:`a value with a size of at least ${e}`,representation:{id:\"effect/schema/isMinSize\",payload:{minSize:e}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isMinSize(${e})`}),[Lr]:!0,arbitraryConstraint:{minSize:e},...t})}function uZ(e,t){return e=xr(e),Ue(r=>r.size<=e,{expected:`a value with a size of at most ${e}`,representation:{id:\"effect/schema/isMaxSize\",payload:{maxSize:e}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isMaxSize(${e})`}),[Lr]:!0,arbitraryConstraint:{maxSize:e},...t})}function fZ(e,t,r){return e=xr(e),t=xr(t),Ue(n=>n.size>=e&&n.size<=t,{expected:e===t?`a value with a size of ${e}`:`a value with a size between ${e} and ${t}`,representation:{id:\"effect/schema/isBetweenSize\",payload:{minimum:e,maximum:t}},toJsonSchema:()=>[{},!0],toCode:()=>({runtime:`Schema.isBetweenSize(${e}, ${t})`}),[Lr]:!0,arbitraryConstraint:{minSize:e,maxSize:t},...r})}function lZ(e,t){return e=xr(e),Ue(r=>Reflect.ownKeys(r).length>=e,{expected:`a value with at least ${e===1?\"1 entry\":`${e} entries`}`,representation:{id:\"effect/schema/isMinProperties\",payload:{minProperties:e}},toJsonSchema:({type:r})=>r===\"object\"?{minProperties:e}:[{minProperties:e},!0],toCode:()=>({runtime:`Schema.isMinProperties(${e})`}),[Lr]:!0,arbitraryConstraint:{minProperties:e},...t})}function pZ(e,t){return e=xr(e),Ue(r=>Reflect.ownKeys(r).length<=e,{expected:`a value with at most ${e===1?\"1 entry\":`${e} entries`}`,representation:{id:\"effect/schema/isMaxProperties\",payload:{maxProperties:e}},toJsonSchema:({type:r})=>r===\"object\"?{maxProperties:e}:[{maxProperties:e},!0],toCode:()=>({runtime:`Schema.isMaxProperties(${e})`}),[Lr]:!0,arbitraryConstraint:{maxProperties:e},...t})}function dZ(e,t,r){return e=xr(e),t=xr(t),Ue(n=>Reflect.ownKeys(n).length>=e&&Reflect.ownKeys(n).length<=t,{expected:e===t?`a value with exactly ${e===1?\"1 entry\":`${e} entries`}`:`a value with between ${e} and ${t} entries`,representation:{id:\"effect/schema/isBetweenProperties\",payload:{minimum:e,maximum:t}},toJsonSchema:({type:n})=>n===\"object\"?{minProperties:e,maxProperties:t}:[{minProperties:e,maxProperties:t},!0],toCode:()=>({runtime:`Schema.isBetweenProperties(${e}, ${t})`}),[Lr]:!0,arbitraryConstraint:{minProperties:e,maxProperties:t},...r})}function mZ(e,t){let r=$u(e),n=vF(r.ast);return Ue((o,s,a)=>{let i=Reflect.ownKeys(o),c=[];for(let u of i){let f=n(u,a);if(f!==void 0&&(c.push(new ut([u],f)),a.errors===\"first\"))break}return or(c)?new wt(s,c,o,a):!0},{expected:\"an object with property names matching the schema\",representation:{id:\"effect/schema/isPropertyNames\",payload:null,schemas:[r.ast]},toJsonSchema:({schemas:o,type:s})=>s===\"object\"?{propertyNames:o[0]}:[{propertyNames:o[0]},!0],toCode:({schemas:o})=>({runtime:`Schema.isPropertyNames(${o[0].runtime})`}),[Lr]:!0,...t})}function uw(e){return Ue(t=>zd(t).length===t.length,{expected:\"an array with unique items\",representation:{id:\"effect/schema/isUnique\",payload:null},toJsonSchema:()=>({uniqueItems:!0}),toCode:()=>({runtime:\"Schema.isUnique()\"}),arbitraryConstraint:{uniqueBy:R},...e})}function eD(e){return Ue(t=>zd(t.map(([r])=>r)).length===t.length,{expected:\"an array with unique keys\",representation:{id:\"effect/schema/isUniqueKey\",payload:null},toCode:()=>({runtime:\"Schema.isUniqueKey()\"}),arbitraryConstraint:{uniqueBy:t=>t[0]},...e})}var fw=K.check(QU()),hZ=K.check(iw(1,1)),tD=e=>(e?.includeStack===!0?1:0)|(e?.excludeCause===!0?2:0),rD=e=>{switch(e){case 0:return;case 1:return{includeStack:!0};case 2:return{excludeCause:!0};case 3:return{includeStack:!0,excludeCause:!0}}},cU=[];function xZ(e){let t=tD(e),r=cU[t];if(r!==void 0)return r;let n=rD(t),o=ba(globalThis.Error,{representation:{id:\"effect/schema/Error\",payload:n??null},toCode:()=>({runtime:n!==void 0?`Schema.ErrorInstance(${F(n)})`:\"Schema.ErrorInstance()\",Type:\"globalThis.Error\"}),expected:\"Error\",toCodecJson:()=>Oe()($8,lM(n))});return cU[t]=o,o}var uU=[];function gZ(e){let t=tD(e),r=uU[t];if(r!==void 0)return r;let n=Nx.pipe(G(vp,pM(rD(t))));return uU[t]=n,n}function yZ(e,t){let r=qr()([e,t],([n,o])=>{let s=$e(Qo([n,o]));return(a,i,c)=>a instanceof globalThis.Map?Vt(nt(s)([...a],c),{onSuccess:u=>new globalThis.Map(u),onFailure:u=>Br(i,\"entries\",u,a,c)}):P(new ft(i,a,c))},{representation:{id:\"effect/schema/ReadonlyMap\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.ReadonlyMap(${n[0].runtime}, ${n[1].runtime})`,Type:`globalThis.ReadonlyMap<${n[0].Type}, ${n[1].Type}>`}),expected:\"ReadonlyMap\",toCodec:([n,o])=>Oe()($e(Qo([n,o])),Pe({decode:s=>new globalThis.Map(s),encode:s=>[...s.entries()]}))});return q(r.ast,{key:e,value:t})}function SZ(e){let t=qr()([e],([r])=>{let n=$e(r);return(o,s,a)=>o instanceof globalThis.Set?Vt(nt(n)([...o],a),{onSuccess:i=>new globalThis.Set(i),onFailure:i=>Br(s,\"values\",i,o,a)}):P(new ft(s,o,a))},{representation:{id:\"effect/schema/ReadonlySet\",payload:null},toCode:({typeParameters:r})=>({runtime:`Schema.ReadonlySet(${r[0].runtime})`,Type:`globalThis.ReadonlySet<${r[0].Type}>`}),expected:\"ReadonlySet\",toCodec:([r])=>Oe()($e(r),Pe({decode:n=>new globalThis.Set(n),encode:n=>[...n.values()]}))});return q(t.ast,{value:e})}var bZ=ba(globalThis.RegExp,{representation:{id:\"effect/schema/RegExp\",payload:null},toCode:()=>({runtime:\"Schema.RegExp\",Type:\"globalThis.RegExp\"}),expected:\"RegExp\",toCodecJson:()=>Oe()(Me({source:K,flags:K}),Yt({decode:(e,t)=>Qc({try:()=>new globalThis.RegExp(e.source,e.flags),catch:()=>new de({expected:\"valid RegExp source and flags\"},e,t)}),encode:e=>g({source:e.source,flags:e.flags})}))}),nD=K.annotate({expected:\"a string that will be decoded as a URL\"}),Ox=ba(globalThis.URL,{representation:{id:\"effect/schema/URL\",payload:null},toCode:()=>({runtime:\"Schema.URL\",Type:\"globalThis.URL\"}),expected:\"URL\",toCodecJson:()=>Oe()(nD,hI)}),EZ=nD.pipe(G(Ox,hI)),oD=K.annotate({expected:\"a string that will be decoded as a Date\"}),Wu=Be(e=>e instanceof globalThis.Date&&!globalThis.Number.isNaN(e.getTime()),{representation:{id:\"effect/schema/Date\",payload:null},toCode:()=>({runtime:\"Schema.Date\",Type:\"globalThis.Date\"}),expected:\"a valid Date\",toCodecJson:()=>Oe()(oD,dI)}),IZ=oD.pipe(G(Wu,dI)),TZ=lo.pipe(G(Wu,sM)),wZ=K.annotate({expected:\"a string that will be decoded as JSON\",contentMediaType:\"application/json\"});function lw(e,t){return wZ.pipe(G(e,IM(t)))}var CZ=lw(vp),sD=ba(globalThis.File,{representation:{id:\"effect/schema/File\",payload:null},toCode:()=>({runtime:\"Schema.File\",Type:\"globalThis.File\"}),expected:\"File\",toCodecJson:()=>Oe()(Me({data:K.check(KU()),type:K,name:K,lastModified:lo}),Yt({decode:(e,t)=>wr(Ei(e.data),{onFailure:()=>P(new de({expected:\"a valid Base64 string\"},e.data,t)),onSuccess:r=>{let n=new globalThis.Uint8Array(r);return g(new globalThis.File([n],e.name,{type:e.type,lastModified:e.lastModified}))}}),encode:(e,t)=>Zc({try:async()=>{let r=new globalThis.Uint8Array(await e.arrayBuffer());return{data:su(r),type:e.type,name:e.name,lastModified:e.lastModified}},catch:()=>new de({expected:\"a readable File\"},e,t)})}))}),aD=ba(globalThis.FormData,{representation:{id:\"effect/schema/FormData\",payload:null},toCode:()=>({runtime:\"Schema.FormData\",Type:\"globalThis.FormData\"}),expected:\"FormData\",toCodecJson:()=>Oe()($e(Qo([K,it([Me({_tag:Hi(\"String\"),value:K}),Me({_tag:Hi(\"File\"),value:sD})])])),Yt({decode:e=>{let t=new globalThis.FormData;for(let[r,n]of e)t.append(r,n.value);return g(t)},encode:e=>g(globalThis.Array.from(e.entries()).map(([t,r])=>typeof r==\"string\"?[t,{_tag:\"String\",value:r}]:[t,{_tag:\"File\",value:r}]))}))});function AZ(e){return aD.pipe(G(e,TM))}var iD=ba(globalThis.URLSearchParams,{representation:{id:\"effect/schema/URLSearchParams\",payload:null},toCode:()=>({runtime:\"Schema.URLSearchParams\",Type:\"globalThis.URLSearchParams\"}),expected:\"URLSearchParams\",toCodecJson:()=>Oe()(K.annotate({expected:\"a query string that will be decoded as URLSearchParams\"}),Pe({decode:e=>new globalThis.URLSearchParams(e),encode:e=>e.toString()}))});function kZ(e){return iD.pipe(G(e,wM))}var vZ=K.annotate({expected:\"a string that will be decoded as a number\"}).pipe(G(Wi,pu)),OZ=K.annotate({expected:\"a string that will be decoded as a finite number\"}).pipe(G(qu,pu)),_Z=q(GI).pipe(G(Bu,Mh)),cD=K.check(JU()),RZ=K.annotate({expected:\"a string that will be decoded as a trimmed string\"}).pipe(G(cD,nM())),PZ=K.annotate({expected:\"a base64 encoded string that will be decoded as a UTF-8 string\"}).pipe(G(K,yM)),MZ=K.annotate({expected:\"a base64 (URL) encoded string that will be decoded as a UTF-8 string\"}).pipe(G(K,SM)),FZ=K.annotate({expected:\"a hex encoded string that will be decoded as a UTF-8 string\"}).pipe(G(K,bM)),NZ=K.annotate({expected:\"a URI component encoded string that will be decoded as a UTF-8 string\"}).pipe(G(K,EM)),KT=it([qu,OU,K]),UZ=Me({issues:$e(Me({message:K,path:Lu($e(it([KT,Me({key:KT})])))}))}),DZ=fo([0,1]).pipe(G(XT,Pe({decode:e=>e===1,encode:e=>e?1:0}))),uD=K.annotate({expected:\"a base64 encoded string that will be decoded as Uint8Array\",format:\"byte\",contentEncoding:\"base64\"}),_x=ba(globalThis.Uint8Array,{representation:{id:\"effect/schema/Uint8Array\",payload:null},toCode:()=>({runtime:\"Schema.Uint8Array\",Type:\"globalThis.Uint8Array\"}),expected:\"Uint8Array\",toCodecJson:()=>Oe()(uD,xI)}),LZ=uD.pipe(G(_x,xI)),$Z=K.annotate({expected:\"a base64 (URL) encoded string that will be decoded as a Uint8Array\"}).pipe(G(_x,{decode:WP(),encode:vh()})),BZ=K.annotate({expected:\"a hex encoded string that will be decoded as a Uint8Array\"}).pipe(G(_x,{decode:KP(),encode:Oh()})),fD=Yt({decode:(e,t)=>{let r=iR(e);return fe(r)?P(new de({expected:\"a valid BigDecimal string\"},e,t)):g(r.value)},encode:e=>g(ws(e))}),jZ=Op({order:nu,formatter:e=>ws(e)}),zZ=_p({order:nu,formatter:e=>ws(e)}),qZ=Rp({order:nu,formatter:e=>ws(e)}),HZ=Pp({order:nu,formatter:e=>ws(e)}),WZ=Mp({order:nu,formatter:e=>ws(e)}),lD=K.annotate({expected:\"a string that will be decoded as a BigDecimal\"}),pD=Be(iE,{representation:{id:\"effect/schema/BigDecimal\",payload:null},toCode:()=>({runtime:\"Schema.BigDecimal\",Type:\"BigDecimal.BigDecimal\",importDeclarations:['import * as BigDecimal from \"effect/BigDecimal\"']}),expected:\"BigDecimal\",toCodecJson:()=>Oe()(lD,fD),toFormatter:()=>e=>ws(e)}),JZ=lD.pipe(G(pD,fD)),Ju=Be(gR,{representation:{id:\"effect/schema/ByteSize\",payload:null},toCode:()=>({runtime:\"Schema.ByteSize\",Type:\"ByteSize.ByteSize\",importDeclarations:['import * as ByteSize from \"effect/ByteSize\"']}),expected:\"ByteSize\",toCodecJson:()=>Oe()(Bu.check(VU(globalThis.BigInt(0))),Pe({decode:xR,encode:uh})),toCodecStringTree:()=>Oe()(dD,mI)}),dD=K.annotate({expected:\"a string that will be decoded as a ByteSize\"}),KZ=dD.pipe(G(Ju,mI)),GZ=Bu.pipe(G(Ju,aM)),VZ=Wi.pipe(G(Ju,iM));function GT(e,t){let r=qr()([e,t],([n,o])=>(s,a,i)=>{if(!tb(s))return P(new ft(a,s,i));switch(s._tag){case\"Fail\":return Vt(nt(n)(s.error,i),{onSuccess:jc,onFailure:c=>Br(a,\"error\",c,s,i)});case\"Die\":return Vt(nt(o)(s.defect,i),{onSuccess:zc,onFailure:c=>Br(a,\"defect\",c,s,i)});case\"Interrupt\":return g(s)}},{representation:{id:\"effect/schema/CauseReason\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.CauseReason(${n[0].runtime}, ${n[1].runtime})`,Type:`Cause.Failure<${n[0].Type}, ${n[1].Type}>`,importDeclarations:['import * as Cause from \"effect/Cause\"']}),expected:\"Cause.Failure\",toCodec:([n,o])=>Oe()(it([Me({_tag:Ct(\"Fail\"),error:n}),Me({_tag:Ct(\"Die\"),defect:o}),Me({_tag:Ct(\"Interrupt\"),fiberId:Ax(qu)})]),Pe({decode:s=>{switch(s._tag){case\"Fail\":return jc(s.error);case\"Die\":return zc(s.defect);case\"Interrupt\":return nb(s.fiberId)}},encode:s=>{switch(s._tag){case\"Fail\":return{_tag:\"Fail\",error:s.error};case\"Die\":return{_tag:\"Die\",defect:s.defect};case\"Interrupt\":return{_tag:\"Interrupt\",fiberId:s.fiberId}}}}))});return q(r.ast,{error:e,defect:t})}function VT(e,t){let r=qr()([e,t],([n,o])=>{let s=$e(GT(n,o));return(a,i,c)=>eb(a)?Vt(nt(s)(a.reasons,c),{onSuccess:oa,onFailure:u=>Br(i,\"failures\",u,a,c)}):P(new ft(i,a,c))},{representation:{id:\"effect/schema/Cause\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.Cause(${n[0].runtime}, ${n[1].runtime})`,Type:`Cause.Cause<${n[0].Type}, ${n[1].Type}>`,importDeclarations:['import * as Cause from \"effect/Cause\"']}),expected:\"Cause\",toCodec:([n,o])=>Oe()($e(GT(n,o)),Pe({decode:oa,encode:({reasons:s})=>s}))});return q(r.ast,{error:e,defect:t})}function fU(e){return Oe()(e,Pe({decode:hE,encode:Ye}))}function ZZ(e){let t=qr()([e],([r])=>{let n=$e(r);return(o,s,a)=>lh(o)?Vt(nt(n)(Ye(o),a),{onSuccess:hE,onFailure:i=>Br(s,\"values\",i,o,a)}):P(new ft(s,o,a))},{representation:{id:\"effect/schema/Chunk\",payload:null},toCode:({typeParameters:r})=>({runtime:`Schema.Chunk(${r[0].runtime})`,Type:`Chunk.Chunk<${r[0].Type}>`}),expected:\"Chunk\",toCodec:([r])=>fU($e(r)),toCodecArbitrary:({constraint:r,typeParameters:[n]})=>fU(Cx($e(n),r?.minLength,r?.maxLength)),toEquivalence:([r])=>dE(r)});return q(t.ast,{value:e})}function mD(){return gt((e,t)=>Wt(yE(e),{onNone:()=>P(new de({message:\"Invalid DateTime input\"},e,t)),onSome:r=>g(SE(r))}))}var YZ=Pe({decode:e=>EE(e),encode:e=>e.offset}),hD=Yt({decode:(e,t)=>Wt(PR(e),{onNone:()=>P(new de({expected:\"a valid IANA time zone\"},e,t)),onSome:g}),encode:e=>g(e.id)}),xD=Yt({decode:(e,t)=>Wt(MR(e),{onNone:()=>P(new de({expected:\"a valid time zone\"},e,t)),onSome:g}),encode:e=>g(Dl(e))}),gD=Yt({decode:(e,t)=>Wt(yE(e),{onNone:()=>P(new de({expected:\"a valid UTC DateTime string\"},e,t)),onSome:r=>g(SE(r))}),encode:e=>g(UR(e))}),yD=Yt({decode:(e,t)=>Wt(RR(e),{onNone:()=>P(new de({expected:\"a valid Zoned DateTime string\"},e,t)),onSome:g}),encode:e=>g(IE(e))}),SD=-864e13,bD=864e13,QZ=SD+840*60*1e3,XZ=bD-840*60*1e3,e5=-720*60*1e3,t5=840*60*1e3,ED=[\"UTC\",\"Europe/London\",\"America/New_York\",\"Asia/Tokyo\",\"Australia/Sydney\"];function ID(e,t,r){let n=Math.max(t,e?.minimum===void 0?t:e.minimum.epochMilliseconds+(e.exclusiveMinimum===!0?1:0)),o=Math.min(r,e?.maximum===void 0?r:e.maximum.epochMilliseconds-(e.exclusiveMaximum===!0?1:0));return n<=o?[n,o]:[t,r]}function pw(e,t){return lo.check(Hu({minimum:e,maximum:t}))}function TD(){return it([pw(e5,t5),fo(ED)])}var Rx=Be(e=>gE(e)&&AR(e),{representation:{id:\"effect/schema/DateTimeUtc\",payload:null},toCode:()=>({runtime:\"Schema.DateTimeUtc\",Type:\"DateTime.Utc\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.Utc\",toCodecArbitrary:({constraint:e})=>{let[t,r]=ID(e,SD,bD);return ju()(pw(t,r),oe(OR))},toCodecJson:()=>Oe()(K,gD),toFormatter:()=>e=>e.toString()}),r5=Wu.pipe(G(Rx,{decode:mD(),encode:oe(FR)})),n5=K.annotate({expected:\"a string that will be decoded as a DateTime.Utc\"}).pipe(G(Rx,gD)),o5=lo.pipe(G(Rx,{decode:mD(),encode:oe(NR)})),s5=Be(wR,{representation:{id:\"effect/schema/TimeZoneOffset\",payload:null},toCode:()=>({runtime:\"Schema.TimeZoneOffset\",Type:\"DateTime.TimeZone.Offset\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.TimeZone.Offset\",toCodecJson:()=>Oe()(lo,YZ),toFormatter:()=>e=>Dl(e)}),wD=K.annotate({expected:\"an IANA time zone identifier\"}),CD=Be(CR,{representation:{id:\"effect/schema/TimeZoneNamed\",payload:null},toCode:()=>({runtime:\"Schema.TimeZoneNamed\",Type:\"DateTime.TimeZone.Named\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.TimeZone.Named\",toCodecArbitrary:()=>ju()(fo(ED),oe(bE)),toCodecJson:()=>Oe()(wD,hD),toFormatter:()=>e=>Dl(e)}),a5=wD.pipe(G(CD,hD)),AD=K.annotate({expected:\"a time zone string (IANA identifier or offset like +03:00)\"}),kD=Be(TR,{representation:{id:\"effect/schema/TimeZone\",payload:null},toCode:()=>({runtime:\"Schema.TimeZone\",Type:\"DateTime.TimeZone\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.TimeZone\",toCodecArbitrary:()=>ju()(TD(),oe(e=>typeof e==\"number\"?EE(e):bE(e))),toCodecJson:()=>Oe()(AD,xD),toFormatter:()=>e=>Dl(e)}),i5=AD.pipe(G(kD,xD)),vD=K.annotate({expected:\"a zoned DateTime string (e.g. 2024-01-01T00:00:00.000+00:00[Europe/London])\"}),OD=Be(e=>gE(e)&&kR(e),{representation:{id:\"effect/schema/DateTimeZoned\",payload:null},toCode:()=>({runtime:\"Schema.DateTimeZoned\",Type:\"DateTime.Zoned\",importDeclarations:['import * as DateTime from \"effect/DateTime\"']}),expected:\"DateTime.Zoned\",toCodecArbitrary:({constraint:e})=>{let[t,r]=ID(e,QZ,XZ);return ju()(Me({epochMilliseconds:pw(t,r),timeZone:TD()}),oe(({epochMilliseconds:n,timeZone:o})=>_R(n,{timeZone:o})))},toCodecJson:()=>Oe()(vD,yD),toFormatter:()=>e=>IE(e),toEquivalence:()=>vR}),c5=vD.pipe(G(OD,yD)),po=(e,t,r,n)=>K.pipe(G(e,Yt({decode:(o,s)=>{let a=t(o);return mt(a)?g(a.success):P(new de({message:a.failure.message},o,s))},encode:o=>g(r(o))}))).annotate({identifier:n}),Ji=Be(Pu,{identifier:\"MacAddress\"}),Ku=po(Ji,qN,PT,\"MacAddressFromString\"),Px=Be(hr,{identifier:\"Ipv4Address\"}),dw=po(Px,Mu,uo,\"Ipv4AddressFromString\"),mw=Be(Yo,{identifier:\"Ipv6Address\"}),_D=po(mw,Sp,uo,\"Ipv6AddressFromString\"),Ki=Be(jN,{identifier:\"IpAddress\"}),Gu=po(Ki,bp,uo,\"IpAddressFromString\"),Xe=(e,t,r,n)=>FU(t,{expected:r})(e).annotate({identifier:n}),u5=Xe(Ki,Fu,\"a multicast address\",\"IpMulticastAddress\"),f5=Xe(Gu,Fu,\"a multicast address\",\"IpMulticastAddressFromString\"),l5=Xe(Ji,Fu,\"a multicast address\",\"MacMulticastAddress\"),p5=Xe(Ku,Fu,\"a multicast address\",\"MacMulticastAddressFromString\"),d5=Xe(Ki,UT,\"a unicast address\",\"IpUnicastAddress\"),m5=Xe(Gu,UT,\"a unicast address\",\"IpUnicastAddressFromString\"),h5=Xe(Ji,mx,\"a unicast address\",\"MacUnicastAddress\"),x5=Xe(Ku,mx,\"a unicast address\",\"MacUnicastAddressFromString\"),g5=Xe(Px,xx,\"a broadcast address\",\"Ipv4BroadcastAddress\"),y5=Xe(dw,xx,\"a broadcast address\",\"Ipv4BroadcastAddressFromString\"),S5=Xe(Ji,dx,\"a broadcast address\",\"MacBroadcastAddress\"),b5=Xe(Ku,dx,\"a broadcast address\",\"MacBroadcastAddressFromString\"),E5=Xe(Ki,NT,\"a loopback address\",\"IpLoopbackAddress\"),I5=Xe(Gu,NT,\"a loopback address\",\"IpLoopbackAddressFromString\"),T5=Xe(Ki,DT,\"a link-local address\",\"IpLinkLocalAddress\"),w5=Xe(Gu,DT,\"a link-local address\",\"IpLinkLocalAddressFromString\"),C5=Xe(Ki,hx,\"an unspecified address\",\"IpUnspecifiedAddress\"),A5=Xe(Gu,hx,\"an unspecified address\",\"IpUnspecifiedAddressFromString\"),k5=Xe(Px,LT,\"a private address\",\"Ipv4PrivateAddress\"),v5=Xe(dw,LT,\"a private address\",\"Ipv4PrivateAddressFromString\"),O5=Xe(mw,$T,\"a unique-local address\",\"Ipv6UniqueLocalAddress\"),_5=Xe(_D,$T,\"a unique-local address\",\"Ipv6UniqueLocalAddressFromString\"),R5=Xe(Ji,MT,\"a locally administered address\",\"MacLocallyAdministeredAddress\"),P5=Xe(Ku,MT,\"a locally administered address\",\"MacLocallyAdministeredAddressFromString\"),M5=Xe(Ji,FT,\"a universally administered address\",\"MacUniversallyAdministeredAddress\"),F5=Xe(Ku,FT,\"a universally administered address\",\"MacUniversallyAdministeredAddressFromString\"),RD=Be(GN,{identifier:\"Ipv4Interface\"}),N5=po(RD,yx,Tp,\"Ipv4InterfaceFromString\"),PD=Be(VN,{identifier:\"Ipv6Interface\"}),U5=po(PD,Sx,Tp,\"Ipv6InterfaceFromString\"),MD=Be(Ep,{identifier:\"IpInterface\"}),D5=po(MD,bx,Tp,\"IpInterfaceFromString\"),FD=Be(YN,{identifier:\"Ipv4Network\"}),L5=po(FD,XN,Cp,\"Ipv4NetworkFromString\"),ND=Be(QN,{identifier:\"Ipv6Network\"}),$5=po(ND,eU,Cp,\"Ipv6NetworkFromString\"),UD=Be(wp,{identifier:\"IpNetwork\"}),B5=po(UD,tU,Cp,\"IpNetworkFromString\"),j5=Be(gp,{identifier:\"InetAddressV4\"}),z5=Be(yp,{identifier:\"InetAddressV6\"}),DD=Be(_T,{identifier:\"InetAddress\"}),q5=po(DD,JN,gx,\"InetAddressFromString\"),LD=Be(px,{identifier:\"UnixPathAddress\"}),H5=K.pipe(G(LD,Pe({decode:KN,encode:e=>e.path}))).annotate({identifier:\"UnixPathAddressFromString\"}),W5=Be(zN,{identifier:\"SocketAddress\"}),J5=Yt({decode:(e,t)=>Wt(kA(e),{onNone:()=>P(new de({expected:\"a valid Duration string\"},e,t)),onSome:g}),encode:e=>g(globalThis.String(e))}),K5=Yt({decode:e=>g(Ro(e)),encode:(e,t)=>Wt(OA(e),{onNone:()=>P(new de({expected:\"a Duration representable as a bigint\"},e,t)),onSome:r=>g(r)})}),G5=Pe({decode:e=>Js(e),encode:e=>Cr(e)}),Mx=Be(Ey,{representation:{id:\"effect/schema/Duration\",payload:null},toCode:()=>({runtime:\"Schema.Duration\",Type:\"Duration.Duration\",importDeclarations:['import * as Duration from \"effect/Duration\"']}),expected:\"Duration\",toCodecJson:()=>Oe()(it([Me({_tag:Ct(\"Infinity\")}),Me({_tag:Ct(\"NegativeInfinity\")}),Me({_tag:Ct(\"Nanos\"),value:Bu}),Me({_tag:Ct(\"Millis\"),value:lo})]),Pe({decode:e=>{switch(e._tag){case\"Infinity\":return ls;case\"NegativeInfinity\":return Ac;case\"Nanos\":return Ro(e.value);case\"Millis\":return Js(e.value)}},encode:e=>{switch(e.value._tag){case\"Infinity\":return{_tag:\"Infinity\"};case\"NegativeInfinity\":return{_tag:\"NegativeInfinity\"};case\"Nanos\":return{_tag:\"Nanos\",value:e.value.nanos};case\"Millis\":return{_tag:\"Millis\",value:e.value.millis}}}}))}),V5=K.annotate({expected:\"a string that will be decoded as a Duration\"}),hw=V5.pipe(G(Mx,J5)),Z5=Bu.pipe(G(Mx,K5)),Y5=Wi.pipe(G(Mx,G5));function Q5(e,t,r){let n=qr()([e,t,r],([o,s,a])=>{let i=VT(s,a);return(c,u,f)=>{if(!hl(c))return P(new ft(u,c,f));switch(c._tag){case\"Success\":return Vt(nt(o)(c.value,f),{onSuccess:vr,onFailure:p=>Br(u,\"value\",p,c,f)});case\"Failure\":return Vt(nt(i)(c.cause,f),{onSuccess:sn,onFailure:p=>Br(u,\"cause\",p,c,f)})}}},{representation:{id:\"effect/schema/Exit\",payload:null},toCode:({typeParameters:o})=>({runtime:`Schema.Exit(${o[0].runtime}, ${o[1].runtime}, ${o[2].runtime})`,Type:`Exit.Exit<${o[0].Type}, ${o[1].Type}, ${o[2].Type}>`,importDeclarations:['import * as Exit from \"effect/Exit\"']}),expected:\"Exit\",toCodec:([o,s,a])=>Oe()(it([Me({_tag:Ct(\"Success\"),value:o}),Me({_tag:Ct(\"Failure\"),cause:VT(s,a)})]),Pe({decode:i=>i._tag===\"Success\"?vr(i.value):sn(i.cause),encode:i=>Ve(i)?{_tag:\"Success\",value:i.value}:{_tag:\"Failure\",cause:i.cause}}))});return q(n.ast,{value:e,error:t,defect:r})}function lU(e,t,r){return Me({type:Ct(e),nodes:$e(Me({index:zi,data:t})),edges:$e(Me({index:zi,source:zi,target:zi,data:r}))})}function pU(e,t){let r=-1,n=new Set;for(let o=0;o{let o=Hl(r),s=Hl(n);if(o.type!==s.type||o.nodes.length!==s.nodes.length||o.edges.length!==s.edges.length)return!1;for(let a=0;a{if(n===null)return bh({type:e,nodes:[],edges:[]});let o=n.nodes.map((s,a)=>({index:a,data:s}));return bh({type:e,nodes:o,edges:n.edges.map(([s,a,i],c)=>({index:c,source:s%o.length,target:a%o.length,data:i}))})}))}function r8(e,t,r){let n=qr()([t,r],([o,s])=>{let a=lU(e,o,s);return(i,c,u)=>!QE(i)||i.mutable||i.type!==e?P(new ft(c,i,u)):I(nt(a)(Hl(i),u),f=>pU(f,u))},{representation:{id:\"effect/schema/Graph\",payload:e},toCode:({typeParameters:o})=>({runtime:`Schema.Graph(${F(e)}, ${o[0].runtime}, ${o[1].runtime})`,Type:`Graph.Graph<${o[0].Type}, ${o[1].Type}, ${F(e)}>`,importDeclarations:['import * as Graph from \"effect/Graph\"']}),expected:`an immutable ${e} Graph`,toCodec:([o,s])=>Oe()(lU(e,o,s),Yt({decode:pU,encode:(a,i)=>X5(a,e,i)})),toCodecArbitrary:({typeParameters:[o,s]})=>t8(e,o,s),toEquivalence:([o,s])=>e8(o,s)});return q(n.ast,{type:e,node:t,edge:r})}function dU(e){return Oe()(e,Pe({decode:PE,encode:zl}))}function n8(e,t){let r=qr()([e,t],([n,o])=>{let s=$e(Qo([n,o]));return(a,i,c)=>GR(a)?Vt(nt(s)(zl(a),c),{onSuccess:PE,onFailure:u=>Br(i,\"entries\",u,a,c)}):P(new ft(i,a,c))},{representation:{id:\"effect/schema/HashMap\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.HashMap(${n[0].runtime}, ${n[1].runtime})`,Type:`HashMap.HashMap<${n[0].Type}, ${n[1].Type}>`,importDeclarations:['import * as HashMap from \"effect/HashMap\"']}),expected:\"HashMap\",toCodec:([n,o])=>dU($e(Qo([n,o]))),toCodecArbitrary:({constraint:n,typeParameters:[o,s]})=>dU(Cx($e(Qo([o,s])).check(eD()),n?.minSize,n?.maxSize)),toEquivalence:([n,o])=>ld(n,o)});return q(r.ast,{key:e,value:t})}function mU(e){return Oe()(e,Pe({decode:NE,encode:Ye}))}function o8(e){let t=qr()([e],([r])=>{let n=$e(r);return(o,s,a)=>XR(o)?Vt(nt(n)(Ye(o),a),{onSuccess:NE,onFailure:i=>Br(s,\"values\",i,o,a)}):P(new ft(s,o,a))},{representation:{id:\"effect/schema/HashSet\",payload:null},toCode:({typeParameters:r})=>({runtime:`Schema.HashSet(${r[0].runtime})`,Type:`HashSet.HashSet<${r[0].Type}>`}),expected:\"HashSet\",toCodec:([r])=>mU($e(r)),toCodecArbitrary:({constraint:r,typeParameters:[n]})=>mU(Cx($e(n).check(uw()),r?.minSize,r?.maxSize)),toEquivalence:([r])=>wf(r)});return q(t.ast,{value:e})}function Gi(e){let t=qr()([e],([r])=>(n,o,s)=>wc(n)?fe(n)?aa:Vt(nt(r)(n.value,s),{onSuccess:O,onFailure:a=>Br(o,\"value\",a,n,s)}):P(new ft(o,n,s)),{representation:{id:\"effect/schema/Option\",payload:null},toCode:({typeParameters:r})=>({runtime:`Schema.Option(${r[0].runtime})`,Type:`Option.Option<${r[0].Type}>`,importDeclarations:['import * as Option from \"effect/Option\"']}),expected:\"Option\",toCodec:([r])=>Oe()(it([Me({_tag:Ct(\"Some\"),value:r}),Me({_tag:Ct(\"None\")})]),Pe({decode:n=>n._tag===\"None\"?v():O(n.value),encode:n=>be(n)?{_tag:\"Some\",value:n.value}:{_tag:\"None\"}}))});return q(t.ast,{value:e})}function s8(e){return rw(e).pipe(G(Gi(Hr(e)),dM()))}function a8(e){return Ax(e).pipe(G(Gi(Hr(e)),mM()))}function i8(e,t){return PU(e).pipe(G(Gi(Hr(e)),hM(t)))}var $D=Be(nP,{representation:{id:\"effect/http/Cookie\",payload:null},toCode:()=>({runtime:\"Schema.Cookie\",Type:\"Cookies.Cookie\",importDeclarations:['import * as Cookies from \"effect/http/Cookies\"']}),expected:\"Cookie\"}),BD=Be(rP,{representation:{id:\"effect/http/Cookies\",payload:null},toCode:()=>({runtime:\"Schema.Cookies\",Type:\"Cookies.Cookies\",importDeclarations:['import * as Cookies from \"effect/http/Cookies\"']}),expected:\"Cookies\",toCodecJson:()=>Oe()($e(K),Pe({decode:oP,encode:uP})),toCodecIso:()=>Oe()(_s(K,$D),Pe({decode:jE,encode:e=>e.cookies}))}),c8=BD.pipe(G(_s(K,K),Pe({decode:cP,encode:e=>zE(globalThis.Object.entries(e).map(([t,r])=>iP(t,r)))}))),u8=Be(dP,{representation:{id:\"effect/http/Headers\",payload:null},toCode:()=>({runtime:\"Schema.Headers\",Type:\"Headers.Headers\",importDeclarations:['import * as Headers from \"effect/http/Headers\"']}),expected:\"Headers\",toEquivalence:()=>KE,toCodec:()=>Oe()(_s(K,K),Pe({decode:mP,encode:e=>({...e})}))}),xw=Be(VE,{representation:{id:\"effect/http/UrlParams\",payload:null},toCode:()=>({runtime:\"Schema.UrlParams\",Type:\"UrlParams.UrlParams\",importDeclarations:['import * as UrlParams from \"effect/http/UrlParams\"']}),expected:\"UrlParams\",toEquivalence:()=>ZE,toCodec:()=>Oe()($e(Qo([K,K])),Pe({decode:Sh,encode:e=>e.params}))}),f8=(e,t)=>xw.pipe(G(lw(vp,t),Yt({decode:r=>Wt(gP(r,e),{onNone:()=>P(new ut([e],new ma(void 0))),onSome:g}),encode:r=>g(Sh([[e,r]]))}))),l8=xw.pipe(G(_s(K,it([K,tw(K)])),Pe({decode:yP,encode:hP})));function p8(e){return qi(e).pipe(G(Gi(Hr(e)),xM()))}function d8(e){return Lu(e).pipe(G(Gi(Hr(e)),gM()))}function m8(e,t){let r=t===void 0?\"omit\":t.onNoneEncoding,n=r===null?null:void 0;return Lu(rw(e)).pipe(G(Gi(Hr(e)),Ph({decode:o=>o.pipe(Va(cd),O),encode:r===\"omit\"?Ga:o=>O(Ff(Ga(o),()=>n))})))}function Fx(e,t){let r=typeof t?.label==\"string\"?t.label:void 0,n=t?.disallowJsonEncode===!0,o=r!==void 0?n?{label:r,disallowJsonEncode:!0}:{label:r}:n?{disallowJsonEncode:!0}:void 0,s=r!==void 0?nt(Ct(r)):void 0,a=qr()([e],([i])=>(c,u,f)=>{if(qE(c)){let p=s!==void 0?Ul(s(c.label,f),d=>new ut([\"label\"],d)):Q;return st(p,()=>Vt(nt(i)(au(c),f),{onSuccess:d=>ki(d,{label:c.label}),onFailure:()=>new wt(u,[new ut([\"value\"],new de(void 0,c,f))],c,f)}))}return P(new ft(u,c,f))},{representation:{id:\"effect/schema/Redacted\",payload:o??null},toCode:({typeParameters:i})=>({runtime:o!==void 0?`Schema.Redacted(${i[0].runtime}, ${F(o)})`:`Schema.Redacted(${i[0].runtime})`,Type:`Redacted.Redacted<${i[0].Type}>`,importDeclarations:['import * as Redacted from \"effect/Redacted\"']}),expected:\"Redacted\",toCodecJson:([i])=>Oe()(i,{decode:oe(c=>ki(c,{label:r})),encode:n?Ah(c=>\"Cannot serialize Redacted\"+(be(c)&&typeof c.value.label==\"string\"?` with label: \"${c.value.label}\"`:\"\")):oe(au)}),toEquivalence:([i])=>pP(i)});return q(a.ast,{value:e})}function h8(e,t){return G(Fx(Hr(e),{label:t?.label,disallowJsonEncode:t?.disallowEncode}),{decode:oe(r=>ki(r,{label:t?.label})),encode:t?.disallowEncode?Ah(r=>\"Cannot encode Redacted\"+(be(r)&&typeof r.value.label==\"string\"?` with label: \"${r.value.label}\"`:\"\")):oe(au)})(e)}function x8(e,t){let r=qr()([e,t],([n,o])=>(s,a,i)=>{if(!iA(s))return P(new ft(a,s,i));switch(s._tag){case\"Success\":return Vt(nT(n)(s.success,i),{onSuccess:B,onFailure:c=>Br(a,\"success\",c,s,i)});case\"Failure\":return Vt(nT(o)(s.failure,i),{onSuccess:W,onFailure:c=>Br(a,\"failure\",c,s,i)})}},{representation:{id:\"effect/schema/Result\",payload:null},toCode:({typeParameters:n})=>({runtime:`Schema.Result(${n[0].runtime}, ${n[1].runtime})`,Type:`Result.Result<${n[0].Type}, ${n[1].Type}>`,importDeclarations:['import * as Result from \"effect/Result\"']}),expected:\"Result\",toCodec:([n,o])=>Oe()(it([Me({_tag:Ct(\"Success\"),success:n}),Me({_tag:Ct(\"Failure\"),failure:o})]),Pe({decode:s=>s._tag===\"Success\"?B(s.success):W(s.failure),encode:s=>mt(s)?{_tag:\"Success\",success:s.success}:{_tag:\"Failure\",failure:s.failure}}))});return q(r.ast,{success:e,failure:t})}var g8=globalThis.Symbol.for(\"immer-draftable\"),ZT={};function gw(e,t,r,n,o){let s=S8(r,t,n),a=jD(t),i=class extends e{constructor(...[c,u]){let p=u?.[\"~payload\"],d=p?.token===ZT?p.value:r.make(c??{},u);super(d,{...u,disableChecks:!0,\"~payload\":{token:ZT,value:d}})}static[Ap]=Ap;get[a](){return a}static[g8]=!0;static identifier=t;static fields=r.fields;static get ast(){return s(this).ast}static pipe(){return Z(this,arguments)}static rebuild(c){return s(this).rebuild(c)}static make(c,u){return Yh(s(this))(c??{},u)}static makeOption(c,u){return Zh(s(this))(c??{},u)}static makeEffect(c,u){return s(this).makeEffect(c??{},u)}static annotate(c){return this.rebuild(to(this.ast,c))}static annotateKey(c){return this.rebuild(cp(this.ast,c))}static check(...c){return this.rebuild(Vo(this.ast,c))}static extend(c){return(u,f)=>{let p=Fp(u)?u:Me(u),d={...r.fields,...p.fields},m=Lh(d,r.ast.checks,{identifier:c});return gw(this,c,ew(Vo(m,p.ast.checks),d),f,o)}}static mapFields(c,u){return r.mapFields(c,u)}};return o!==void 0&&Object.assign(i.prototype,o(t)),i}function y8(e){return new ve(oe(t=>new e(t,{\"~payload\":{token:ZT,value:t}})),jr())}function jD(e){return`~effect/Schema/Class/${e}`}function S8(e,t,r){let n;return o=>{if(n!==void 0)return n;let s=jD(t),a=u=>u instanceof o||k(u,s),i=y8(o),c=q(new yu([e.ast],()=>(u,f,p)=>a(u)?g(u):P(new ft(f,u,p)),{identifier:t,[Ih]:([u])=>({isConstructed:a,link:new at(u,i)}),toCodec:([u])=>new at(u.ast,i),toEquivalence:([u])=>u,toFormatter:([u])=>f=>`${o.identifier}(${u(f)})`,[Wl]:hu(e.ast),...r}));return n=G(c,i)(e)}}function Fp(e){return Tx(e)}var zD=e=>(t,r)=>{let n=Fp(t)?t:Me(t);return gw(Kd,e,n,r,o=>({toString(){return`${o}(${F({...this})})`}}))},b8=e=>(t,r,n)=>{let o=Fp(r)?r.mapFields(s=>({_tag:Hi(t),...s}),{unsafePreserveChecks:!0}):kx(t,r);return zD(e??t)(o,n)},qD=e=>(t,r)=>{let n=Fp(t)?t:Me(t);return gw(_f,e,n,r,s=>({name:s}))},E8=e=>(t,r,n)=>{let o=Fp(r)?r.mapFields(s=>({_tag:Hi(t),...s}),{unsafePreserveChecks:!0}):kx(t,r);return qD(e??t)(o,n)};function I8(e){return t=>t.annotate({toFormatter:e})}function T8(e,t){return _N(e.ast,t)}function w8(e){return t=>t.annotate({toEquivalence:e})}function C8(e){return bT(e.ast)}function A8(e,t){return mp(e.ast,t)}function k8(e,t){let r=mp(Cu(e.ast),t);return ix(r,t)}var v8=rx,O8=nx,Np=ox,_8=oN;function R8(e,t){return CN(e,t)}function P8(e){return DN(e)}function M8(e){return LN(e)}function F8(e){return $N(e)}function N8(e,t){return r=>q(to(r.ast,{toCodecIso:()=>new at(e.ast,Rh(t))}),{schema:r})}function U8(e){return wN(e)}function D8(e){let t=MU(()=>r),r=it([e,$e(t),_s(K,t)]);return r}var Nx=q(to(xu,{toCode:()=>({runtime:\"Schema.Json\",Type:\"Schema.Json\"})})),L8=_s(K,Nx),$8=Me({message:K,name:qi(K),stack:qi(K),cause:qi(Nx)}),B8=q(to(bF,{toCode:()=>({runtime:\"Schema.MutableJson\",Type:\"Schema.MutableJson\"})}));function j8(e){return As(e.ast)}function z8(e){return e.ast.context?.annotations}var HD=\"~effect/MutableHashMap\";var q8={[HD]:HD,[Symbol.iterator](){return this.backing[Symbol.iterator]()},toString(){return`MutableHashMap(${F(Array.from(this))})`},toJSON(){return{_id:\"MutableHashMap\",values:vt(Array.from(this))}},[Te](){return this.toJSON()},pipe(){return Z(this,arguments)}},Up=()=>{let e=Object.create(q8);return e.backing=new Map,e.buckets=new Map,e};var Xo=l(2,(e,t)=>{if(e.backing.has(t))return O(e.backing.get(t));if(yw(t))return v();let r=H(t),n=e.buckets.get(r);return n===void 0?v():H8(e,n,t)}),yw=e=>typeof e!=\"object\"&&typeof e!=\"function\";var H8=(e,t,r)=>{for(let n=0,o=t.length;n{if(e.backing.has(t)||yw(t))return e.backing.set(t,r),e;let n=H(t),o=e.buckets.get(n);if(o===void 0)return e.buckets.set(n,[t]),e.backing.set(t,r),e;let s=W8(o,t);return s===void 0&&(o.push(t),s=t),e.backing.set(s,r),e}),W8=(e,t)=>{for(let r=0,n=e.length;r{if(yw(t))return e.backing.delete(t),e;let r=H(t),n=e.buckets.get(r);if(n===void 0)return e;for(let o=0,s=n.length;o(e.backing.clear(),e.buckets.clear(),e),WD=e=>e.backing.size;var JD=Qy;var Ea=Xy,KD=hk,mo=rl;var Zu=ds,Ia=dO;var Pr=JS,GD=bO;var VD=\"~effect/MutableRef\",J8={[VD]:VD,...rr,toJSON(){return{_id:\"MutableRef\",current:vt(this.current)}}},Yu=e=>{let t=Object.create(J8);return t.current=e,t};var ZD=e=>e.current;var Lx=l(2,(e,t)=>(e.current=t,e));var Xt=Symbol.for(\"effect/MutableList/Empty\"),Ps=()=>({head:void 0,tail:void 0,length:0}),Ew=e=>{let t=e.head;t.offset>=1024&&t===e.tail&&t.mutable&&(t.array.length-t.offset)*8<=t.offset&&(e.head=e.tail={array:t.array.slice(t.offset),mutable:!0,offset:0,next:void 0})},YD=()=>({array:[],mutable:!0,offset:0,next:void 0}),ho=(e,t)=>{e.tail?e.tail.mutable||(e.tail.next=YD(),e.tail=e.tail.next):e.head=e.tail=YD(),e.tail.array.push(t),e.length++},Iw=(e,t)=>{e.head={array:[t],mutable:!0,offset:0,next:e.head},e.tail||(e.tail=e.head),e.length++};var $x=(e,t)=>Bx(e,Ye(t),!Array.isArray(t)),Bx=(e,t,r=!1)=>{if(t.length===0)return 0;let n={array:t,mutable:r,offset:0,next:void 0};return e.head?e.tail=e.tail.next=n:e.head=e.tail=n,e.length+=t.length,t.length},Rs=e=>{e.head=e.tail=void 0,e.length=0},Vi=(e,t)=>{if(t=ot(t),t<=0||!e.head)return[];if(t=Math.min(t,e.length),t===e.length&&e.head?.offset===0&&!e.head.next){let s=e.head.array;return Rs(e),s}let r=new Array(t),n=0,o=e.head;for(;o;){for(;o.offset{if(t=ot(t),t<=0||!e.head)return;if(t=Math.min(t,e.length),t===e.length&&e.head?.offset===0&&!e.head.next){Rs(e);return}let r=0,n=e.head;for(;n;){let o=n.array.length-n.offset;if(r+o>t){n.offset+=t-r,e.head=n,e.length-=t,Ew(e);return}r+=o,n=n.next}Rs(e)},Ta=e=>Vi(e,e.length),Zi=e=>{if(!e.head)return Xt;let t=e.head.array[e.head.offset];return e.head.mutable&&(e.head.array[e.head.offset]=void 0),e.head.offset++,e.length--,e.head.offset===e.head.array.length?e.head.next?e.head=e.head.next:Rs(e):Ew(e),t},QD=(e,t)=>{if(t=ot(t),t<=0)return[];let r=Math.min(t,e.length),n=new Array(r),o=0,s=e.head;for(;s;){for(let a=s.offset;a{let r=[],n=e.head,o=0;for(;n;){for(let s=n.offset;sTw(e,r=>r!==t);var K8=\"~effect/PubSub\";var G8=\"~effect/PubSub/Subscription\",Jx=e=>X(()=>eY(e.atomicPubSub(),new Map,kn(),Pr(!1),Yu(!1),e.strategy(),Yu(v()))),Kx=e=>Jx({atomicPubSub:()=>Nw(e),strategy:()=>new qx}),Gx=e=>Jx({atomicPubSub:()=>Nw(e),strategy:()=>new Hx}),Vx=e=>Jx({atomicPubSub:()=>Nw(e),strategy:()=>new Fw}),Zx=e=>Jx({atomicPubSub:()=>e1(e),strategy:()=>new Hx}),Nw=e=>{let t=typeof e==\"number\"?{capacity:e}:e;if(tY(t.capacity),t.capacity===1/0)return e1(t);let r=t.replay&&t.replay>0?new Wx(Math.ceil(t.replay)):void 0;return t.capacity===1?new vw(r):T_(t.capacity)===t.capacity?new Aw(t.capacity,r):new ww(t.capacity,r)},e1=e=>{let t=e?.replay;return new _w(t&&t>0?new Wx(Math.ceil(t)):void 0)};var $p=e=>Ts(cn(t=>(Lx(e.shutdownFlag,!0),Le(e.scope,pb(t.id)).pipe(_r(e.strategy.shutdown),nh(e.shutdownHook.open),ia)))),Yx=l(2,(e,t)=>X(()=>V8(e,t))),V8=l(2,(e,t)=>{if(e.shutdownFlag.current||be(e.ended.current))return!1;if(Lx(e.ended,O(t)),e.strategy instanceof qx)for(let[n,o,s]of Ta(e.strategy.publishers))s&&Do(o,vr(!1));let r=vr(t);for(let n of e.subscribers.values())for(let o of n){let s;for(;(s=Zi(o))!==Xt;)Do(s,r)}return e.subscribers.clear(),!0});var Qx=l(2,(e,t)=>$(()=>e.shutdownFlag.current||be(e.ended.current)?g(!1):e.pubsub.publish(t)?(e.strategy.completeSubscribersUnsafe(e.pubsub,e.subscribers),g(!0)):e.strategy.handleSurplus(e.pubsub,e.subscribers,[t],e.shutdownFlag,e.ended)));var t1=l(2,(e,t)=>$(()=>{if(e.shutdownFlag.current||be(e.ended.current))return g(!1);let r=e.pubsub.publishAll(t);return e.strategy.completeSubscribersUnsafe(e.pubsub,e.subscribers),r.length===0?g(!0):e.strategy.handleSurplus(e.pubsub,e.subscribers,r,e.shutdownFlag,e.ended)})),Bp=e=>Ts(Ml(t=>{let r=Fe(t,Or),n=Nt(e.scope),o=X8(e.pubsub,e.subscribers,e.strategy,e.ended);return Kt(n,Z8(o)).pipe(_r(Lo(r,s=>Le(n,s))),Tt(o))})),Z8=e=>Ts(cn(t=>(Lx(e.shutdownFlag,!0),sa(Ta(e.pollers),r=>xb(r,t.id),{discard:!0,concurrency:\"unbounded\"}).pipe(Rr(()=>X(()=>{e.subscribers.delete(e.subscription),e.subscription.unsubscribe(),e.replayWindow.close(),e.strategy.onPubSubEmptySpaceUnsafe(e.pubsub,e.subscribers)})),nh(e.shutdownHook.open),ia)))),Xx=e=>$(()=>{if(e.shutdownFlag.current)return _n;if(e.replayWindow.remaining>0){let r=e.replayWindow.take();return g(r)}let t=e.pollers.length===0?e.subscription.poll():Xt;return t===Xt?n1(e):(e.strategy.onPubSubEmptySpaceUnsafe(e.pubsub,e.subscribers),g(t))}),r1=e=>$(function t(r){if(e.shutdownFlag.current)return _n;let n=e.pollers.length===0?e.subscription.pollUpTo(Number.POSITIVE_INFINITY):[];return r&&(n=r.concat(n)),e.strategy.onPubSubEmptySpaceUnsafe(e.pubsub,e.subscribers),e.replayWindow.remaining>0?g(e.replayWindow.takeAll().concat(n)):or(n)?g(n):I(n1(e),o=>t([o]))}),n1=e=>Yc(t=>{if(e.shutdownFlag.current)return t(_n);if(be(e.ended.current)){let o=e.pollers.length===0?e.subscription.poll():Xt;return o!==Xt?(e.strategy.onPubSubEmptySpaceUnsafe(e.pubsub,e.subscribers),t(g(o))):t(g(e.ended.current.value))}let r=ui(),n=e.subscribers.get(e.subscription);return n||(n=new Set,e.subscribers.set(e.subscription,n)),n.add(e.pollers),ho(e.pollers,r),e.strategy.completePollersUnsafe(e.pubsub,e.subscribers,e.subscription,e.pollers),r.effect?t(r.effect):(r.resumes=[t],X(()=>XD(e.pollers,r)))});var bt=Symbol.for(\"effect/PubSub/AbsentValue\"),Y8=(e,t,r)=>{e.has(t)||e.set(t,new Set),e.get(t).add(r)},Q8=(e,t,r)=>{if(!e.has(t))return;let n=e.get(t);n.delete(r),n.size===0&&e.delete(t)},X8=(e,t,r,n)=>new Pw(e,t,e.subscribe(),Ps(),Pr(!1),Yu(!1),r,e.replayWindow(),n),ww=class{array;replayIndices;publisherIndex=0;subscribers;subscriberCount=0;subscribersIndex=0;capacity;replayBuffer;constructor(t,r){this.capacity=t,this.replayBuffer=r,this.array=Array.from({length:t}),this.replayIndices=r?Array.from({length:t}):[],this.subscribers=Array.from({length:t})}replayWindow(){return this.replayBuffer?new Qu(this.replayBuffer):eg}isEmpty(){return this.publisherIndex===this.subscribersIndex}isFull(){return this.publisherIndex===this.subscribersIndex+this.capacity}size(){return this.publisherIndex-this.subscribersIndex}publish(t){if(this.isFull())return!1;let r=this.replayBuffer?.offer(t);if(this.subscriberCount!==0){let n=this.publisherIndex%this.capacity;this.array[n]=t,r!==void 0&&(this.replayIndices[n]=r),this.subscribers[n]=this.subscriberCount,this.publisherIndex+=1}return!0}publishAll(t){if(this.subscriberCount===0)return this.replayBuffer&&this.replayBuffer.offerAll(t),[];let r=Ye(t),n=r.length,o=this.publisherIndex-this.subscribersIndex,s=this.capacity-o,a=Math.min(n,s);if(a===0)return r;let i=0,c=this.publisherIndex+a;for(;this.publisherIndex!==c;){let u=r[i++],f=this.publisherIndex%this.capacity;this.array[f]=u;let p=this.replayBuffer?.offer(u);p!==void 0&&(this.replayIndices[f]=p),this.subscribers[f]=this.subscriberCount,this.publisherIndex+=1}return r.slice(i)}slide(){if(this.subscribersIndex!==this.publisherIndex){let t=this.subscribersIndex%this.capacity,r=this.array[t];this.array[t]=bt,this.subscribers[t]=0,this.subscribersIndex+=1,this.replayBuffer?.slide(r,this.replayIndices[t])}}subscribe(){return this.subscriberCount+=1,new Cw(this,this.publisherIndex,!1)}},Cw=class{self;subscriberIndex;unsubscribed;constructor(t,r,n){this.self=t,this.subscriberIndex=r,this.unsubscribed=n}isEmpty(){return this.unsubscribed||this.self.publisherIndex===this.subscriberIndex||this.self.publisherIndex===this.self.subscribersIndex}size(){return this.unsubscribed?0:this.self.publisherIndex-Math.max(this.subscriberIndex,this.self.subscribersIndex)}poll(){if(this.unsubscribed)return Xt;if(this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex),this.subscriberIndex!==this.self.publisherIndex){let t=this.subscriberIndex%this.self.capacity,r=this.self.array[t];return this.self.subscribers[t]-=1,this.self.subscribers[t]===0&&(this.self.array[t]=bt,this.self.subscribersIndex+=1),this.subscriberIndex+=1,r}return Xt}pollUpTo(t){if(t=ot(t),this.unsubscribed)return[];this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex);let r=this.self.publisherIndex-this.subscriberIndex,n=Math.min(t,r);if(n<=0)return[];let o=[],s=this.subscriberIndex+n;for(;this.subscriberIndex!==s;){let a=this.subscriberIndex%this.self.capacity,i=this.self.array[a];this.self.subscribers[a]-=1,this.self.subscribers[a]===0&&(this.self.array[a]=bt,this.self.subscribersIndex+=1),o.push(i),this.subscriberIndex+=1}return o}unsubscribe(){if(!this.unsubscribed)for(this.unsubscribed=!0,this.self.subscriberCount-=1,this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex);this.subscriberIndex!==this.self.publisherIndex;){let t=this.subscriberIndex%this.self.capacity;this.self.subscribers[t]-=1,this.self.subscribers[t]===0&&(this.self.array[t]=bt,this.self.subscribersIndex+=1),this.subscriberIndex+=1}}},Aw=class{array;replayIndices;mask;publisherIndex=0;subscribers;subscriberCount=0;subscribersIndex=0;capacity;replayBuffer;constructor(t,r){this.capacity=t,this.replayBuffer=r,this.array=Array.from({length:t}),this.replayIndices=r?Array.from({length:t}):[],this.mask=t-1,this.subscribers=Array.from({length:t})}replayWindow(){return this.replayBuffer?new Qu(this.replayBuffer):eg}isEmpty(){return this.publisherIndex===this.subscribersIndex}isFull(){return this.publisherIndex===this.subscribersIndex+this.capacity}size(){return this.publisherIndex-this.subscribersIndex}publish(t){if(this.isFull())return!1;let r=this.replayBuffer?.offer(t);if(this.subscriberCount!==0){let n=this.publisherIndex&this.mask;this.array[n]=t,r!==void 0&&(this.replayIndices[n]=r),this.subscribers[n]=this.subscriberCount,this.publisherIndex+=1}return!0}publishAll(t){if(this.subscriberCount===0)return this.replayBuffer&&this.replayBuffer.offerAll(t),[];let r=Ye(t),n=r.length,o=this.publisherIndex-this.subscribersIndex,s=this.capacity-o,a=Math.min(n,s);if(a===0)return r;let i=0,c=this.publisherIndex+a;for(;this.publisherIndex!==c;){let u=r[i++],f=this.publisherIndex&this.mask;this.array[f]=u;let p=this.replayBuffer?.offer(u);p!==void 0&&(this.replayIndices[f]=p),this.subscribers[f]=this.subscriberCount,this.publisherIndex+=1}return r.slice(i)}slide(){if(this.subscribersIndex!==this.publisherIndex){let t=this.subscribersIndex&this.mask,r=this.array[t];this.array[t]=bt,this.subscribers[t]=0,this.subscribersIndex+=1,this.replayBuffer?.slide(r,this.replayIndices[t])}}subscribe(){return this.subscriberCount+=1,new kw(this,this.publisherIndex,!1)}},kw=class{self;subscriberIndex;unsubscribed;constructor(t,r,n){this.self=t,this.subscriberIndex=r,this.unsubscribed=n}isEmpty(){return this.unsubscribed||this.self.publisherIndex===this.subscriberIndex||this.self.publisherIndex===this.self.subscribersIndex}size(){return this.unsubscribed?0:this.self.publisherIndex-Math.max(this.subscriberIndex,this.self.subscribersIndex)}poll(){if(this.unsubscribed)return Xt;if(this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex),this.subscriberIndex!==this.self.publisherIndex){let t=this.subscriberIndex&this.self.mask,r=this.self.array[t];return this.self.subscribers[t]-=1,this.self.subscribers[t]===0&&(this.self.array[t]=bt,this.self.subscribersIndex+=1),this.subscriberIndex+=1,r}return Xt}pollUpTo(t){if(t=ot(t),this.unsubscribed)return[];this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex);let r=this.self.publisherIndex-this.subscriberIndex,n=Math.min(t,r);if(n<=0)return[];let o=[],s=this.subscriberIndex+n;for(;this.subscriberIndex!==s;){let a=this.subscriberIndex&this.self.mask,i=this.self.array[a];this.self.subscribers[a]-=1,this.self.subscribers[a]===0&&(this.self.array[a]=bt,this.self.subscribersIndex+=1),o.push(i),this.subscriberIndex+=1}return o}unsubscribe(){if(!this.unsubscribed)for(this.unsubscribed=!0,this.self.subscriberCount-=1,this.subscriberIndex=Math.max(this.subscriberIndex,this.self.subscribersIndex);this.subscriberIndex!==this.self.publisherIndex;){let t=this.subscriberIndex&this.self.mask;this.self.subscribers[t]-=1,this.self.subscribers[t]===0&&(this.self.array[t]=bt,this.self.subscribersIndex+=1),this.subscriberIndex+=1}}},vw=class{publisherIndex=0;subscriberCount=0;subscribers=0;value=bt;replayIndex=0;capacity=1;replayBuffer;constructor(t){this.replayBuffer=t}replayWindow(){return this.replayBuffer?new Qu(this.replayBuffer):eg}pipe(){return Z(this,arguments)}isEmpty(){return this.subscribers===0}isFull(){return!this.isEmpty()}size(){return this.isEmpty()?0:1}publish(t){if(this.isFull())return!1;let r=this.replayBuffer?.offer(t);return this.subscriberCount!==0&&(this.value=t,r!==void 0&&(this.replayIndex=r),this.subscribers=this.subscriberCount,this.publisherIndex+=1),!0}publishAll(t){if(this.subscriberCount===0)return this.replayBuffer&&this.replayBuffer.offerAll(t),[];let r=Ye(t);return r.length===0?r:this.publish(r[0])?r.slice(1):r}slide(){if(this.isFull()){let t=this.value;this.subscribers=0,this.value=bt,this.replayBuffer?.slide(t,this.replayIndex)}}subscribe(){return this.subscriberCount+=1,new Ow(this,this.publisherIndex,!1)}},Ow=class{self;subscriberIndex;unsubscribed;constructor(t,r,n){this.self=t,this.subscriberIndex=r,this.unsubscribed=n}isEmpty(){return this.unsubscribed||this.self.subscribers===0||this.subscriberIndex===this.self.publisherIndex}size(){return this.isEmpty()?0:1}poll(){if(this.isEmpty())return Xt;let t=this.self.value;return this.self.subscribers-=1,this.self.subscribers===0&&(this.self.value=bt),this.subscriberIndex=this.self.publisherIndex,t}pollUpTo(t){return ot(t)<1||this.isEmpty()?[]:[this.poll()]}unsubscribe(){this.unsubscribed||(this.unsubscribed=!0,this.self.subscriberCount-=1,this.self.subscribers!==0&&this.subscriberIndex!==this.self.publisherIndex&&(this.self.subscribers-=1,this.self.subscribers===0&&(this.self.value=bt)))}},_w=class{publisherHead={value:bt,replayIndex:void 0,subscribers:0,next:null};publisherTail=this.publisherHead;publisherIndex=0;subscribersIndex=0;capacity=Number.MAX_SAFE_INTEGER;replayBuffer;constructor(t){this.replayBuffer=t}replayWindow(){return this.replayBuffer?new Qu(this.replayBuffer):eg}isEmpty(){return this.publisherHead===this.publisherTail}isFull(){return!1}size(){return this.publisherIndex-this.subscribersIndex}publish(t){let r=this.replayBuffer?.offer(t),n=this.publisherTail.subscribers;if(n!==0){let o={value:t,replayIndex:r,subscribers:n,next:null};this.publisherTail.next=o,this.publisherTail=this.publisherTail.next,this.publisherIndex+=1}return!0}publishAll(t){if(this.publisherTail.subscribers!==0)for(let r of t)this.publish(r);else this.replayBuffer&&this.replayBuffer.offerAll(t);return[]}slide(){if(this.publisherHead!==this.publisherTail){let t=this.publisherHead.next,r=t.value;this.publisherHead=this.publisherHead.next,this.publisherHead.value=bt,this.subscribersIndex+=1,this.replayBuffer?.slide(r,t.replayIndex)}}subscribe(){return this.publisherTail.subscribers+=1,new Rw(this,this.publisherTail,this.publisherIndex,!1)}},Rw=class{self;subscriberHead;subscriberIndex;unsubscribed;constructor(t,r,n,o){this.self=t,this.subscriberHead=r,this.subscriberIndex=n,this.unsubscribed=o}isEmpty(){if(this.unsubscribed)return!0;let t=!0,r=!0;for(;r;)this.subscriberHead===this.self.publisherTail?r=!1:this.subscriberHead.next.value!==bt?(t=!1,r=!1):(this.subscriberHead=this.subscriberHead.next,this.subscriberIndex+=1);return t}size(){return this.unsubscribed?0:this.self.publisherIndex-Math.max(this.subscriberIndex,this.self.subscribersIndex)}poll(){if(this.unsubscribed)return Xt;let t=!0,r=Xt;for(;t;)if(this.subscriberHead===this.self.publisherTail)t=!1;else{let n=this.subscriberHead.next.value;n!==bt&&(r=n,this.subscriberHead.subscribers-=1,this.subscriberHead.subscribers===0&&(this.self.publisherHead=this.self.publisherHead.next,this.self.publisherHead.value=bt,this.self.subscribersIndex+=1),t=!1),this.subscriberHead=this.subscriberHead.next,this.subscriberIndex+=1}return r}pollUpTo(t){t=ot(t);let r=[],n=0;for(;n!==t;){let o=this.poll();o===Xt?n=t:(r.push(o),n+=1)}return r}unsubscribe(){if(!this.unsubscribed)for(this.unsubscribed=!0,this.self.publisherTail.subscribers-=1;this.subscriberHead!==this.self.publisherTail;)this.subscriberHead.next.value!==bt&&(this.subscriberHead.subscribers-=1,this.subscriberHead.subscribers===0&&(this.self.publisherHead=this.self.publisherHead.next,this.self.publisherHead.value=bt,this.self.subscribersIndex+=1)),this.subscriberHead=this.subscriberHead.next}},Pw=class{[G8]={_A:R};pubsub;subscribers;subscription;pollers;shutdownHook;shutdownFlag;strategy;replayWindow;ended;constructor(t,r,n,o,s,a,i,c,u){this.pubsub=t,this.subscribers=r,this.subscription=n,this.pollers=o,this.shutdownHook=s,this.shutdownFlag=a,this.strategy=i,this.replayWindow=c,this.ended=u}pipe(){return Z(this,arguments)}},Mw=class{[K8]={_A:R};pubsub;subscribers;scope;shutdownHook;shutdownFlag;strategy;ended;constructor(t,r,n,o,s,a,i){this.pubsub=t,this.subscribers=r,this.scope=n,this.shutdownHook=o,this.shutdownFlag=s,this.strategy=a,this.ended=i}pipe(){return Z(this,arguments)}},eY=(e,t,r,n,o,s,a)=>new Mw(e,t,r,n,o,s,a),tY=e=>{if(e<=0||e!==1/0&&!Number.isInteger(e))throw new Error(`Cannot construct PubSub with capacity of ${e}`)},qx=class{publishers=Ps();get shutdown(){return cn(t=>sa(Ta(this.publishers),([r,n,o])=>o?xb(n,t.id):Q,{concurrency:\"unbounded\",discard:!0}))}handleSurplus(t,r,n,o,s){return Yc(a=>{if(be(s.current))return a(g(!1));let i=ui();return this.offerUnsafe(n,i),this.onPubSubEmptySpaceUnsafe(t,r),this.completeSubscribersUnsafe(t,r),ZD(o)?(this.removeUnsafe(i),a(_n)):i.effect?a(i.effect):(i.resumes=[a],X(()=>this.removeUnsafe(i)))})}onPubSubEmptySpaceUnsafe(t,r){let n=!0;for(;n&&!t.isFull();){let o=Zi(this.publishers);if(o===Xt)n=!1;else{let[s,a]=o,i=t.publish(s);i&&o[2]?Do(a,vr(!0)):i||Iw(this.publishers,o),this.completeSubscribersUnsafe(t,r)}}}completePollersUnsafe(t,r,n,o){return Uw(this,t,r,n,o)}completeSubscribersUnsafe(t,r){return Dw(this,t,r)}offerUnsafe(t,r){let n=t[Symbol.iterator](),o=n.next();if(!o.done)for(;;){let s=o.value;if(o=n.next(),o.done){ho(this.publishers,[s,r,!0]);break}ho(this.publishers,[s,r,!1])}}removeUnsafe(t){Tw(this.publishers,([r,n])=>n!==t)}},Hx=class{get shutdown(){return Q}handleSurplus(t,r,n,o){return g(!1)}onPubSubEmptySpaceUnsafe(t,r){}completePollersUnsafe(t,r,n,o){return Uw(this,t,r,n,o)}completeSubscribersUnsafe(t,r){return Dw(this,t,r)}},Fw=class{get shutdown(){return Q}handleSurplus(t,r,n,o,s){return X(()=>be(s.current)?!1:(this.slidingPublishUnsafe(t,n),this.completeSubscribersUnsafe(t,r),!0))}onPubSubEmptySpaceUnsafe(t,r){}completePollersUnsafe(t,r,n,o){return Uw(this,t,r,n,o)}completeSubscribersUnsafe(t,r){return Dw(this,t,r)}slidingPublishUnsafe(t,r){let n=r[Symbol.iterator](),o=n.next();if(!o.done&&t.capacity>0){let s=o.value,a=!0;for(;a;){t.slide();let i=t.publish(s);i&&(o=n.next())&&!o.done?s=o.value:i&&(a=!1)}}}},Uw=(e,t,r,n,o)=>{let s=!0;for(;s&&!n.isEmpty();){let a=Zi(o);if(a===Xt)Q8(r,n,o),o.length===0?s=!1:Y8(r,n,o);else{let i=n.poll();i===Xt?Iw(o,a):(Do(a,vr(i)),e.onPubSubEmptySpaceUnsafe(t,r))}}},Dw=(e,t,r)=>{for(let[n,o]of r)for(let s of o)e.completePollersUnsafe(t,r,n,s)},Wx=class{capacity;head={value:bt,index:0,next:null};tail=this.head;slideValues=[];size=0;index=0;publisherIndex=0;constructor(t){this.capacity=t}slide(t,r){this.slideValues[this.index%this.capacity]={value:t,index:r},this.index++}offer(t){let r=this.publisherIndex++;return this.tail.value=t,this.tail.index=r,this.tail.next={value:bt,index:0,next:null},this.tail=this.tail.next,this.size===this.capacity?this.head=this.head.next:this.size+=1,r}offerAll(t){for(let r of t)this.offer(r)}},Qu=class{buffer;values;index=0;remaining;slideIndex;newestIndex=-1;constructor(t){this.buffer=t,this.remaining=t.size,this.slideIndex=t.index,this.values=new Array(this.remaining);let r=t.head;for(let n=0;nthis.newestIndex&&(this.index=(this.index+1)%this.values.length,this.values[(this.index+this.remaining-1)%this.values.length]=s.value,this.newestIndex=s.index)}this.slideIndex=this.buffer.index}take(){if(this.remaining===0)return;this.sync();let t=this.values[this.index];return this.values[this.index]=bt,this.index=(this.index+1)%this.values.length,this.remaining--,this.remaining===0&&this.close(),t}takeN(t){t=ot(t);let r=Math.min(t,this.remaining),n=new Array(r);for(let o=0;o{},takeN:()=>[],takeAll:()=>[],close:()=>{}};var rY=\"~effect/Queue\",nY=\"~effect/Queue/Enqueue\",oY=\"~effect/Queue/Dequeue\";var $w={_A:R,_E:R},sY={[rY]:$w,[nY]:$w,[oY]:$w,...rr,toJSON(){return{_id:\"effect/Queue\",state:this.state._tag,size:cY(this)}}},Mn=e=>re(t=>{let r=Object.create(sY);return r.dispatcher=t.currentDispatcher,r.capacity=e?.capacity??Number.POSITIVE_INFINITY,r.strategy=e?.strategy??\"suspend\",r.messages=Ps(),r.scheduleRunning=!1,r.state={_tag:\"Open\",takers:new Set,offers:new Set,awaiters:new Set},D(r)}),jp=e=>Mn({capacity:e});var o1=()=>Mn(),Sn=l(2,(e,t)=>ne(()=>rg(e,t)?sg:e.state._tag===\"Open\"&&e.strategy===\"suspend\"?pY(e,t):a1)),rg=(e,t)=>e.state._tag!==\"Open\"?!1:e.messages.length>=e.capacity?e.strategy===\"sliding\"?(Zi(e.messages),ho(e.messages,t),!0):e.capacity<=0&&e.state.takers.size>0?(ho(e.messages,t),i1(e),!0):!1:(ho(e.messages,t),tg(e),!0),Yi=l(2,(e,t)=>ht(r=>{let n=ng(e,t);return n.length===0||e.strategy===\"dropping\"?r(Ae(n)):f1(e,{_tag:\"Array\",remaining:n,offset:0,resume:r})})),ng=(e,t)=>{if(e.state._tag!==\"Open\")return Ye(t);if(e.capacity===Number.POSITIVE_INFINITY||e.strategy===\"sliding\")return $x(e.messages,t),e.strategy===\"sliding\"&&Vi(e.messages,e.messages.length-e.capacity),tg(e),[];let r=e.capacity<=0?e.state.takers.size:e.capacity-e.messages.length;if(r===0)return Ye(t);let n=[],o=0;for(let s of t)oz(()=>wa(e,t))),wa=(e,t)=>{if(e.state._tag!==\"Open\")return!1;let r=Ht(t),n=Jk(r,uY);return e.state.offers.size===0&&e.messages.length===0?(ag(e,n),!0):(e.state={...e.state,_tag:\"Closing\",exit:n},tg(e),!0)},jw=e=>Bt(e,gc(Wa())),og=e=>wa(e,gc(Wa()));var Fn=e=>z(()=>aY(e)),aY=e=>{if(e.state._tag===\"Done\")return!1;Rs(e.messages);let t=e.state.offers;ag(e,e.state._tag===\"Open\"?fY:e.state.exit);for(let r of t)l1(r);return!0};var Qi=e=>s1(e,1,Number.POSITIVE_INFINITY);var s1=l(3,(e,t,r)=>(t=ot(t),r=ot(r),c1(()=>lY(e,t,r)??Fo(u1(e,()=>zw(e,t)),s1(e,t,r))))),Ms=e=>c1(()=>iY(e)??Fo(u1(e,()=>zw(e,1)),Ms(e)));var iY=e=>{if(e.state._tag===\"Done\")return e.state.exit;if(e.messages.length>0){let t=Zi(e.messages);return Bw(e),Ae(t)}else if(e.capacity<=0&&e.state.offers.size>0){let t=p1(e.state.offers);return Bw(e),Ae(t)}};var cY=e=>e.state._tag===\"Done\"?0:e.messages.length;var a1=Ae(!1),sg=Ae(!0),uY=en(Wa()),fY=Nc(),i1=e=>{if(!(e.state._tag===\"Done\"||e.state.takers.size===0)){for(let t of e.state.takers)if(t.ready()&&(e.state.takers.delete(t),t.resume(pr),e.messages.length===0))break}},tg=e=>{e.scheduleRunning||e.state._tag===\"Done\"||e.state.takers.size===0||(e.scheduleRunning=!0,e.dispatcher.scheduleTask(()=>{e.scheduleRunning=!1,i1(e)},0))},lY=(e,t,r)=>{if(e.state._tag===\"Done\")return e.state.exit;if(r<=0||t<=0)return Ae([]);if(!zw(e,t))return;let n=e.messages.length>0?Vi(e.messages,r):[p1(e.state.offers)];return Bw(e),Ae(n)},c1=En({op:\"QueueTake\",[Ke](e){let t=this[ue]();return vf(t)?t[Ke](e):t}}),zw=(e,t)=>e.messages.length>=(e.state._tag===\"Closing\"?1:Math.min(t,e.capacity||1))||e.capacity<=0&&e.state._tag!==\"Done\"&&e.state.offers.size>0,u1=(e,t)=>ht(r=>{if(e.state._tag===\"Done\")return r(e.state.exit);if(t())return r(pr);let n={ready:t,resume:r};return e.state.takers.add(n),z(()=>{e.state._tag!==\"Done\"&&e.state.takers.delete(n)})}),pY=(e,t)=>ht(r=>rg(e,t)?r(sg):f1(e,{_tag:\"Single\",message:t,resume:r})),f1=(e,t)=>{if(e.state._tag!==\"Open\")return l1(t);let r=e.state.offers;return r.add(t),z(()=>{e.state._tag!==\"Done\"&&(r.delete(t),e.state._tag===\"Closing\"&&r.size===0&&e.messages.length===0&&ag(e,e.state.exit))})},l1=e=>e._tag===\"Single\"?e.resume(a1):e.resume(Ae(e.remaining.slice(e.offset))),p1=e=>{let t=e.values().next().value;if(t._tag===\"Single\")return e.delete(t),t.resume(sg),t.message;let r=t.remaining[t.offset++];return t.offset===t.remaining.length&&(e.delete(t),t.resume(Ae([]))),r},Bw=e=>{if(e.state._tag===\"Done\")return Bo(e.state.exit.cause);if(e.state.offers.size===0)return e.state._tag===\"Closing\"&&e.messages.length===0?(ag(e,e.state.exit),Bo(e.state.exit.cause)):!1;for(let t of e.state.offers){let r=e.capacity-e.messages.length;if(r<=0)break;if(t._tag===\"Single\")ho(e.messages,t.message),e.state.offers.delete(t),t.resume(sg);else{for(;t.offset{if(e.state._tag===\"Done\")return;let r=e.state;e.state={_tag:\"Done\",exit:t};for(let n of r.takers)n.resume(t);r.takers.clear();for(let n of r.awaiters)n(t);r.awaiters.clear()};var ig=e=>new Hw(e),d1=(e,t,r)=>ht(n=>{if(e.free>=t)return n(r);let o=()=>{e.free{e.waiters.delete(o)})}),Hw=class{waiters=new Set;taken=0;permits;constructor(t){this.permits=t}get free(){return this.permits-this.taken}take(t){let r=ne(()=>this.freethis.free0&&t.currentDispatcher.scheduleTask(()=>{for(let n of this.waiters){if(this.free<=0)break;n()}},0),this.free}resize(t){return re(r=>(this.permits=t,this.free<0?pe:(this.releaseUnsafe(r,0),pe)))}release(t){return re(r=>D(this.releaseUnsafe(r,t)))}get releaseAll(){return re(t=>D(this.releaseUnsafe(t,this.taken)))}withPermits(t){return r=>Hn(n=>{let o=ne(()=>{if(this.freeo)}return this.taken+=t,qn(n(r),()=>{this.releaseUnsafe(ds(),t)},!0)});return o})}withPermit=this.withPermits(1);withPermitsIfAvailable(t){return r=>Hn(n=>this.free{this.releaseUnsafe(ds(),t)},!0)))}};var Ww=e=>hl(e)?Ve(e)?Y(e.value):e:g(e);var y1=\"~effect/Channel\",cr=e=>k(e,y1),dY={[y1]:{_Env:R,_InErr:R,_InElem:R,_OutErr:R,_OutElem:R},pipe(){return Z(this,arguments)}},Ce=e=>{let t=Object.create(dY);return t.transform=(r,n)=>rt(e(r,n),o=>g(ze(o))),t},Nn=(e,t)=>Ce((r,n)=>I(he(e)(r,n),o=>t(o,n))),er=e=>Ce((t,r)=>e),go=e=>Ce(Se(function*(t,r){let n=Nt(r),o=a=>Le(n,Jm(a)),s=yield*Vn(e(t,r,n),o);return Vn(s,o)})),he=e=>e.transform,zp=4096,mY=(e,t,r)=>Mn({capacity:r?.bufferSize,strategy:r?.strategy}).pipe(Rr(n=>Kt(e,Fn(n))),Rr(n=>Es(t(n),e).pipe(rt(o=>Bt(n,o)),Ut(e))));var S1=(e,t)=>Ce((r,n)=>j(mY(n,e,t),Qi)),cg=e=>Ce((t,r)=>$(()=>he(e())(t,r))),hY=(e,t,r)=>go(Se(function*(n,o,s){let a=v();yield*Lo(s,c=>be(a)?r(a.value,c):Q);let i=yield*Ts(j(e,c=>(a=O(c),c)));return yield*he(t(i))(n,o)}));var b1=e=>er(X(()=>{let t=e();return $(()=>{let r=t.next();return r.done?Y(r.value):g(r.value)})})),Jw=e=>er(X(()=>{let t=0;return $(()=>t>=e.length?Y():g(e[t++]))}));var Kw=(e,t=zp)=>{let r=Tn(t);return er(X(()=>{let n=e(),o=v();return $(()=>{if(o._tag===\"Some\")return Y(o.value);let s=[];for(;s.lengthKw(()=>e[Symbol.iterator](),t),ug=e=>fg(g(e));var I1=e=>fg(X(e)),Xu=er(g(Y())),T1=er(g(mi)),es=e=>er(g(P(e))),w1=e=>er(Xm(e)),qp=e=>er(ze(e)),C1=e=>er(Is(e)),Gw=e=>qp(ci(e)),fg=e=>er(X(()=>{let t=!1;return $(()=>t?Y():(t=!0,e))}));var A1=e=>er(I(e,()=>Y())),Vw=e=>er(g(I(e,Ww)));var k1=e=>er(g(Qi(e)));var Zw=e=>er(g(Xb(r1(e),()=>Y())));var v1=e=>Ca(j(Bp(e),Zw)),O1=e=>Ca(j(Bp(e),t=>Vw(Xx(t))));var _1=e=>Ce((t,r)=>xY({scope:r,readable:e.evaluate(),onError:e.onError,releaseLockOnEnd:e.releaseLockOnEnd}));var xY=e=>{let t=e.readable.getReader(),r=e.exit??Yu(void 0),n=$(()=>r.current?r.current:zo(Zc({try:()=>t.read(),catch:e.onError}),{onFailure:o=>r.current??ze(o),onSuccess:({done:o,value:s})=>r.current?r.current:o?Y():g(Ee(s))}));return Tt(Kt(e.scope,e.releaseLockOnEnd?X(()=>t.releaseLock()):Qm(()=>t.cancel().catch($n))),n)},gY=(e,t)=>Ce(Se(function*(r,n){let o=e[Symbol.asyncIterator]();return o.return&&(yield*Kt(n,Qm(()=>o.return()))),I(Zc({try:()=>o.next(),catch:t}),s=>s.done?Y(s.value):g(s.value))})),R1=(e,t)=>yo(gY(e,t),Ee),yo=l(2,(e,t)=>Nn(e,r=>X(()=>{let n=0;return j(r,o=>t(o,n++))})));var P1=l(2,(e,t)=>Nn(e,r=>g(He(r,n=>I(t(n),Y))))),M1=e=>e===void 0||e!==\"unbounded\"&&e<=1,lg=l(e=>cr(e[0]),(e,t,r)=>M1(r?.concurrency)?F1(e,t):yY(e,t,r)),F1=(e,t)=>Ce((r,n)=>{let o=0;return j(he(e)(r,n),I(s=>t(s,o++)))}),yY=(e,t,r)=>go(Se(function*(n,o,s){let a=0,i=yield*he(e)(n,o),c=r.concurrency===\"unbounded\"?Number.MAX_SAFE_INTEGER:r.concurrency,u=yield*jp(0);yield*Kt(s,Fn(u));let f=Rn(yield*hi()),p=Ia(s);if(r.unordered){let d=ig(c),m=je(d.release(1)),x=zo({onFailure:y=>I(Bt(u,y),m),onSuccess:y=>I(Sn(u,y),m)});yield*d.take(1).pipe(I(()=>i),I(y=>{let S=a++;return p(f(x($(()=>t(y,S))))),Q}),$t({disableYield:!0}),rt(y=>d.withPermits(c-1)(Bt(u,y))),Ut(s))}else{let d=yield*jp(c-2);yield*Kt(s,Fn(d)),yield*Ms(d).pipe(qb,I(y=>Sn(u,y)),$t({disableYield:!0}),rt(y=>Bt(u,y)),Ut(s));let m,x=y=>{y._tag!==\"Success\"&&(m=y.cause,wa(u,y.cause))};yield*i.pipe(I(y=>{if(m)return ze(m);let S=a++,w=f($(()=>t(y,S)));return p(w),w.addObserver(x),Sn(d,Ea(w))}),$t({disableYield:!0}),rt(y=>Sn(d,sn(y)).pipe(_r(Bt(d,y)))),Ut(s))}return Ms(u)}));var Yw=l(e=>cr(e[0]),(e,t,r)=>M1(r?.concurrency)?SY(e,t):bY(e,t,r)),SY=(e,t)=>Ce((r,n)=>j(he(e)(r,n),o=>{let s,a,i=I(o,u=>(a??=Nt(n),st(he(t(u))(r,a),f=>(s=c(f),s)))),c=He(u=>{if(s=void 0,a.state._tag===\"Empty\")return i;let f=Le(a,xt);return a=void 0,I(f,()=>i)});return $(()=>s??i)})),bY=(e,t,r)=>e.pipe(yo(t),mg(r)),N1=l(2,(e,t)=>Ce((r,n)=>X(()=>{let o,s=Nt(n),a=I(he(e)(r,s),i=>(o=He(i,c=>Le(s,xt).pipe(I(()=>he(t(c))(r,n)),I(u=>(o=u,u)))),o));return $(()=>o??a)})));var Qw=l(4,(e,t,r,n)=>Ce(Se(function*(o,s){let a=yield*he(e)(o,s),i=yield*he(t)(o,s),c=r();return $(()=>{let u=n(c,a,i);return j(u,([f,p])=>(c=p,f))})}))),U1=l(2,(e,t)=>Ce((r,n)=>X(()=>{let o,s=Nt(n),a=I(he(e)(r,s),i=>{let c=i.pipe(Rr(()=>(o=i,Q)),He(u=>Le(s,vr(u)).pipe(_r(he(t(u))(r,n)),I(f=>(o=f,f)))));return o=c,c});return $(()=>o??a)}))),D1=e=>Yw(e,R),gr=e=>Nn(e,t=>{let r,n=0,o=$(function s(){if(r===void 0)return I(t,i=>{switch(i.length){case 0:return s();case 1:return g(i[0]);default:return r=i,g(i[n++])}});let a=r[n++];return n>=r.length&&(r=void 0,n=0),g(a)});return g(o)}),L1=e=>F1(e,Ww),$1=e=>Nn(e,t=>g($t(t,{disableYield:!0}))),B1=l(2,(e,t)=>Kn(typeof t==\"function\"?t(R):t).pipe(j(r=>{let n=Gt.defaultValue();return j1(yg(e,Gt,X(()=>n)),o=>r(o).pipe(j(s=>{n=s}),He(()=>Y(o))))}),Ca)),j1=(e,t)=>Ce((r,n)=>X(()=>{let o,s=()=>{let a=Nt(n);return I(he(e)(r,a),i=>(o=He(i,c=>Le(a,xt).pipe(I(()=>t(c)),I(s))),o))};return $(()=>o??s())})),z1=e=>j1(e,()=>Q),q1=l(2,(e,t)=>Nn(e,(r,n)=>j(Cl(t),o=>Rr(r,o)))),H1=l(2,(e,t)=>Ce((r,n)=>j(he(e)(r,n),o=>I(o,function s(a){return t(a)?g(a):I(o,s)}))));var W1=l(2,(e,t)=>Nn(e,r=>g(I(r,function n(o){let s=[];for(let a=0;aNn(e,r=>g(I(r,function n(o){let s=[];for(let a=0;aNn(e,r=>{let n=I(r,o=>Gb(o,t));return g(I(n,function o(s){return tt(s)?g(s):I(n,o)}))})),G1=l(2,(e,t)=>Nn(e,r=>g(I(r,function n(o){return I(Vb(o,t),s=>tt(s)?g(s):I(r,n))})))),ef=l(e=>cr(e[0]),(e,t,r,n)=>Ce((o,s)=>j(he(e)(o,s),a=>{let i=t(),c,u=0,f,p=zo(a,{onFailure(m){f=m;let x=n?.onHalt&&n.onHalt(i);return x&&x.length>0?g([i,x]):ze(m)},onSuccess(m){let x=r(i,m);return gA(x)?g(x):x}});return $(function m(){if(c===void 0)return f?ze(f):I(p,([y,S])=>(i=y,S.length===0?m():S.length===1?g(S[0]):(c=S,m())));let x=c[u++];return u>=c.length&&(c=void 0,u=0),g(x)})})));var V1=l(3,(e,t,r)=>Ce((n,o)=>j(he(e)(n,o),s=>{let a=t,i=!0;return $(()=>i?(i=!1,g(a)):j(I(s,c=>r(a,c)),c=>(a=c,a)))}))),tf=l(2,(e,t)=>Ce((r,n)=>{let o=Nt(n);return j(he(e)(r,o),s=>{let a=s.pipe(rt(i=>{if(Bo(i))return ze(i);let c=o;return o=Nt(n),Le(c,sn(i)).pipe(_r(he(t(i))(r,o)),I(u=>(a=u,u)))}));return $(()=>a)})})),Z1=l(2,(e,t)=>dg(e,dl,t)),pg=l(2,(e,t)=>tf(e,r=>A1(I(t(r),n=>ze(r))))),Y1=l(3,(e,t,r)=>tf(e,n=>t(n)?r(n):qp(n))),dg=l(3,(e,t,r)=>tf(e,n=>{let o=t(n);return ce(o)?qp(o.failure):r(o.success,n)})),xo=l(2,(e,t)=>dg(e,ab,r=>t(r)));var Q1=l(2,(e,t)=>Nn(e,r=>g(rh(r,n=>qc(n)?Q:ia(t(n)))))),X1=l(e=>cr(e[0]),(e,t,r,n)=>xo(e,o=>t(o)?r(o):n?n(o):es(o))),e2=l(e=>cr(e[0]),(e,t,r,n)=>xo(e,o=>{let s=t(o);return ce(s)?n?n(s.failure):es(s.failure):r(s.success)}));var t2=l(e=>cr(e[0]),(e,t,r,n,o)=>xo(e,s=>{if(At(s,t)&&k(s,\"reason\")){let a=s.reason;return At(a,r)?n(a,s):o?o(a,s):es(s)}return es(s)})),r2=l(e=>cr(e[0]),(e,t,r,n)=>{let o;return xo(e,s=>{if(At(s,t)&&k(s,\"reason\")&&k(s.reason,\"_tag\")&&Hm(s.reason._tag)){let a=s.reason;return o??=new Set(Object.keys(r)),o.has(a._tag)?r[a._tag](a,s):n?n(a,s):es(s)}return es(s)})});var n2=l(2,(e,t)=>xo(e,r=>es(t(r)))),o2=e=>xo(e,Gw),s2=l(e=>cr(e[0]),(e,t)=>{if(!t?.log)return xo(e,()=>Xu);let r=ih(t.log===!0?void 0:t.log);return xo(pg(e,n=>sb(n)?r(n):Q),()=>Xu)}),x1=e=>tf(e,()=>Xu),a2=l(e=>cr(e[0]),(e,t)=>{if(!t?.log)return x1(e);let r=ih(t.log===!0?void 0:t.log);return x1(pg(e,n=>r(n)))}),i2=l(2,(e,t)=>cg(()=>{let r,n=Gt.defaultValue(),o=yg(e,Gt,X(()=>n)),s=r0(o,()=>(r=void 0,Q)),a=typeof t==\"function\"?t(R):t,i=xo(s,Se(function*(c){return r||(r=yield*Kn(a)),n=yield*r(c),i},(c,u)=>He(c,()=>g(es(u))),Ca));return i})),c2=l(e=>cr(e[0]),(e,t,r)=>e.pipe(yo(t),mg({...r,concurrency:r?.concurrency??1,switch:!0}))),mg=l(2,(e,{bufferSize:t=16,concurrency:r,switch:n=!1})=>go(Se(function*(o,s,a){let i=r===\"unbounded\"?Number.MAX_SAFE_INTEGER:Math.max(1,r),c=n?void 0:ig(i),u=yield*GD(!0),f=new Set,p=yield*jp(t);yield*Kt(a,Fn(p));let d=yield*he(e)(o,s);return yield*dr(function*(){for(;;){let m;c&&(f.size=i;){let E=lA(f);f.delete(E),f.size===0&&(yield*u.open),yield*mo(E)}let w=yield*S.pipe(Rr(()=>Rl),I(E=>Sn(p,E)),$t({disableYield:!0}),Vn(Se(function*(E){let U=Jm(E),_=yield*vn(Le(y,U));if(!f.has(w))return;let h=hb([U,_]);Uo(h)&&(yield*Bt(p,h.cause)),f.delete(w),c&&(yield*c.release(1)),f.size===0&&(yield*u.open)})),eu);u.closeUnsafe(),f.add(w)}}).pipe(rt(m=>{let x=Tl(m);return mt(x)?u.whenOpen(Bt(p,m)):Bt(p,m)}),Ut(a)),Ms(p)}))),hg=l(e=>cr(e[0])&&cr(e[1]),(e,t,r)=>go(Se(function*(n,o,s){let a=r?.haltStrategy??\"both\",i=yield*jp(0);yield*Kt(s,Fn(i));let c=0;function u(p,d){if(c++,!Bo(d))return Bt(i,d);switch(a){case\"both\":return c===2?Bt(i,d):Q;case\"left\":case\"right\":return p===a?Bt(i,d):Q;case\"either\":return Bt(i,d)}}let f=(p,d,m)=>he(d)(n,m).pipe(I(x=>x.pipe(I(y=>Sn(i,y)),$t)),Vn(x=>Fl(Jm(x),y=>Le(m,y)).pipe(zo({onFailure:y=>u(p,y),onSuccess:()=>u(p,x)}))),Ut(s));return yield*f(\"left\",e,Nt(s)),yield*f(\"right\",t,Nt(s)),Ms(i)}))),u2=l(2,(e,t)=>hg(e,A1(t),{haltStrategy:\"left\"})),f2=()=>Ce((e,t)=>X(()=>{let r=\"\",n=!1,o=v();function s(c){let u=[];function f(p){r.length===0?u.push(p):(u.push(r+p),r=\"\")}for(let p=0;po._tag===\"Some\"?Y(o.value):Km(e,{onSuccess:i,onFailure:ze,onDone:c=>{if(o=O(c),r.length>0){let u=r;return r=\"\",n=!1,g([u])}return Y(c)}}));function i(c){let u=s(c);return u!==null?g(u):a}return a}));var Xw=l(2,(e,t)=>Ce((r,n)=>I(he(e)(r,n),o=>he(t)(o,n)))),e0=l(2,(e,t)=>Ce((r,n)=>I(he(e)(r,n),o=>{let s=rt(o,a=>Bo(a)?ze(a):an(ub(a)));return j(he(t)(s,n),a=>Wb(a,i=>qc(i)?ze(i.value):an(i)))}))),Ca=e=>Ce((t,r)=>{let n;return g($(()=>n||e.pipe(Es(r),I(o=>he(o)(t,r)),I(o=>n=o))))}),l2=e=>go((t,r,n)=>j(Es(he(e)(t,r),n),Es(n)));var p2=l(2,(e,t)=>Ce(Se(function*(r,n){let o=yield*he(e)(r,n),s=yield*Mn({capacity:t.capacity===\"unbounded\"?void 0:t.capacity,strategy:t.capacity===\"unbounded\"?void 0:t.strategy});return yield*Kt(n,Fn(s)),yield*o.pipe(I(a=>Sn(s,a)),$t({disableYield:!0}),Vn(a=>Bt(s,a)),Ut(n)),Ms(s)}))),d2=l(2,(e,t)=>Ce(Se(function*(r,n){let o=yield*he(e)(r,n),s=yield*Mn({capacity:t.capacity===\"unbounded\"?void 0:t.capacity,strategy:t.capacity===\"unbounded\"?void 0:t.strategy});return yield*Kt(n,Fn(s)),yield*o.pipe(I(a=>Yi(s,a)),$t({disableYield:!0}),Vn(a=>Bt(s,a)),Ut(n)),Qi(s)}))),t0=l(2,(e,t)=>hg(e,er(g(I(t,Y))),{haltStrategy:\"either\"})),m2=l(2,(e,t)=>go(Se(function*(r,n,o){let s=yield*he(e)(r,n),a=yield*Ut(t,o,{startImmediately:!0});return $(()=>{let i=a.pollUnsafe();return i===void 0?s:mb(i,{onFailure:ze,onSuccess:Y})})}))),h2=l(2,(e,t)=>Hp(e,r=>Uo(r)?t(r.cause):Q)),Hp=l(2,(e,t)=>go((r,n,o)=>Lo(o,t).pipe(_r(he(e)(r,n))))),x2=l(2,(e,t)=>Ca(Tt(t,e))),r0=l(2,(e,t)=>Nn(e,r=>X(()=>{let n=!0,o=Rr(r,s=>(n=!1,t(s)));return $(()=>n?o:r)}))),g2=l(2,(e,t)=>Nn(e,r=>g(He(r,n=>I(t,()=>Y(n)))))),y2=l(2,(e,t)=>Hp(e,r=>t)),Xi=(e,t,r)=>$(()=>{let n=kn(),o=he(e)(Y(),n);return He(I(o,t),r||g).pipe(un(s=>Le(n,s)))});var xg=l(2,(e,t)=>Ce((r,n)=>j(On(he(e)(r,n),t),On(t)))),gg=l(3,(e,t,r)=>Ce((n,o)=>j(qo(he(e)(n,o),t,r),qo(t,r)))),yg=l(3,(e,t,r)=>Ce((n,o)=>I(r,s=>he(gg(e,t,s))(n,o)))),S2=l(e=>cr(e[0]),(e,t,r)=>Uf(t)?xg(e,t):Ce((n,o)=>I(r?.local?yl(t,gl(),o):Lm(t,o),s=>j(On(he(e)(n,o),s),On(s))))),b2=l(2,(e,t)=>Ce((r,n)=>Ml(o=>{let s=t(o);return he(xg(e,s))(r,n)})));var n0=function(){let e=cr(arguments[0]),t=e?arguments[1]:arguments[0],r=Gs(e?arguments[2]:arguments[1]);if(e){let n=arguments[0];return g1(n,t,r)}return n=>g1(n,t,r)},g1=(e,t,r)=>hY(eE(t,r),n=>gg(e,ei,n),(n,o)=>cn(s=>{let a=s.getRef(Nr),i=s.getRef(ZO);return Rm(n,o,a,i)}));var E2=e=>Xi(e,t=>$t(t,{disableYield:!0})),ec=l(2,(e,t)=>Xi(e,r=>$t(I(r,t),{disableYield:!0}))),I2=l(2,(e,t)=>Xi(e,r=>r.pipe(I(t),I(n=>n?Q:Y()),$t({disableYield:!0})))),o0=e=>j(tc(e,()=>({bytes:0,arrays:[]}),(t,r)=>{for(let n=0;n{let n=new Uint8Array(r),o=0;for(let s=0;s$(()=>{let t=v();return Xi(e,r=>r.pipe(eh,I(n=>(t=n,Y()))),()=>g(t))}),w2=e=>$(()=>{let t=Symbol(),r=t;return Xi(e,n=>$t(I(n,o=>(r=o,Q)),{disableYield:!0}),()=>r===t?aa:zb(r))}),tc=l(3,(e,t,r)=>$(()=>{let n=t();return Xi(e,o=>Gn({while:Sr,body:()=>o,step:s=>{n=r(n,s)}}),()=>g(n))})),C2=l(3,(e,t,r)=>$(()=>{let n=t();return Xi(e,o=>Gn({while:Sr,body:je(o.pipe(I(s=>r(n,s)),j(s=>{n=s}))),step:$n}),()=>g(n))})),s0=Se(function*(e){let t=ig(1),r=yield*hi(),n=Fe(r,Or);return(yield*he(e)(Y(),n)).pipe(On(r),t.withPermits(1))},rt(e=>g(ze(e)))),ts=(e,t)=>he(e)(Y(),t);var a0=l(e=>cr(e[0]),(e,t)=>Ho(r=>ec(e,n=>Yi(t,n)).pipe(r,vn,I(n=>(Ve(n)?og(t):wa(t,n.cause),Q)))));var A2=l(e=>cr(e[0]),Se(function*(e,t){let r=yield*xi,n=yield*Mn({capacity:typeof t.capacity==\"number\"?t.capacity:void 0,strategy:typeof t.capacity==\"number\"?t.strategy:void 0});return yield*Kt(r,Fn(n)),yield*Ut(a0(e,n),r),n}));var k2=e=>gi(e.capacity===\"unbounded\"?Zx(e):e.strategy===\"dropping\"?Gx(e):e.strategy===\"sliding\"?Vx(e):Kx(e),$p),v2=l(2,Se(function*(e,t){let r=yield*k2(t);return yield*tu(i0(e,r,{shutdownOnEnd:t.shutdownOnEnd!==!1})),r})),i0=l(e=>cr(e[0]),(e,t,r)=>ec(e,n=>t1(t,n)).pipe(r?.shutdownOnEnd===!0?yi($p(t)):R)),O2=l(2,Se(function*(e,t){let r=yield*k2(t);return yield*ec(e,n=>Qx(r,n)).pipe(un(n=>Yx(r,n)),tu),r}));var Mr={};Eo(Mr,{Array:()=>DX,Boolean:()=>$X,ByteSize:()=>jX,ConfigError:()=>af,Date:()=>JX,Duration:()=>BX,Finite:()=>MX,Int:()=>FX,Literal:()=>NX,Literals:()=>UX,LogLevel:()=>qX,NonEmptyString:()=>RX,Number:()=>PX,Port:()=>zX,Record:()=>LX,Redacted:()=>HX,String:()=>_X,URL:()=>WX,all:()=>XL,fail:()=>vX,flatMap:()=>SX,isConfig:()=>YL,map:()=>QL,mapEffect:()=>bX,nested:()=>KX,option:()=>IX,orElse:()=>EX,schema:()=>tr,succeed:()=>OX,unwrap:()=>t$,withDefault:()=>e$});var sf={};Eo(sf,{ConfigProvider:()=>ac,SourceError:()=>Tg,constantCase:()=>rX,fromDir:()=>gX,fromDotEnv:()=>xX,fromDotEnvContents:()=>JL,fromEnv:()=>WL,fromEnvRecord:()=>R0,fromUnknown:()=>aX,layer:()=>oX,layerAdd:()=>sX,make:()=>Cg,makeArray:()=>O0,makeRecord:()=>wg,makeValue:()=>Yp,mapInput:()=>_0,nested:()=>nX,orElse:()=>v0});var _2=\"~effect/PlatformError\",rf=class extends Fr(\"BadArgument\"){get message(){return`${this.module}.${this.method}${this.description?`: ${this.description}`:\"\"}`}};var R2=class extends Fr(\"PlatformError\"){constructor(t){\"cause\"in t?super({reason:t,cause:t.cause}):super({reason:t})}[_2]=_2;get message(){return this.reason.message}};var IY=\"~effect/Stream\",TY={_R:R,_E:R,_A:R},P2=function(e){this.channel=e};P2.prototype={[IY]:TY,pipe(){return Z(this,arguments)}};var M2=e=>new P2(e);var CY=\"~effect/Sink\";var AY={_A:R,_In:R,_L:R,_E:R,_R:R},kY={[CY]:AY,pipe(){return Z(this,arguments)}};var vY=e=>{let t=Object.create(kY);return t.transform=e,t},F2=e=>Ce((t,r)=>g(I(e.transform(t,r),Y)));var N2=e=>{let t=ot(e);return vY(r=>{let n=[];if(t===0)return g([n]);let o;return r.pipe(I(s=>{if(n.length+s.length<=t)return n.push(...s),n.length===t?Y():Q;for(let a=0;ag([n,o])))})};var ns={};Eo(ns,{DefaultChunkSize:()=>V2,Do:()=>y7,TypeId:()=>G2,accumulate:()=>t7,aggregate:()=>K9,aggregateWithin:()=>b0,as:()=>uQ,bind:()=>b7,bindEffect:()=>E7,bindTo:()=>I7,broadcast:()=>TL,broadcastN:()=>G9,buffer:()=>e9,bufferArray:()=>t9,callback:()=>Q2,catch:()=>Zp,catchCause:()=>uL,catchCauseFilter:()=>l9,catchCauseIf:()=>f9,catchDefect:()=>r9,catchFilter:()=>y0,catchIf:()=>lL,catchReason:()=>i9,catchReasons:()=>c9,catchTag:()=>s9,catchTags:()=>a9,changes:()=>r7,changesWith:()=>wL,changesWithEffect:()=>n7,chunks:()=>yL,collect:()=>e7,combine:()=>_9,combineArray:()=>R9,concat:()=>nc,cross:()=>MQ,crossWith:()=>iL,debounce:()=>D9,decodeText:()=>o7,die:()=>HY,drain:()=>yQ,drainFork:()=>SQ,drop:()=>S0,dropRight:()=>k9,dropUntil:()=>w9,dropUntilEffect:()=>C9,dropWhile:()=>xL,dropWhileEffect:()=>gL,dropWhileFilter:()=>A9,empty:()=>Ns,encodeText:()=>s7,ensuring:()=>d7,fail:()=>nf,failCause:()=>X2,failCauseSync:()=>WY,failSync:()=>qY,filter:()=>JQ,filterEffect:()=>GQ,filterMap:()=>KQ,filterMapEffect:()=>VQ,flatMap:()=>of,flatten:()=>oL,flattenArray:()=>gQ,flattenEffect:()=>lQ,flattenIterable:()=>CQ,flattenTake:()=>AQ,forever:()=>wQ,fromArray:()=>Gp,fromArrayEffect:()=>VY,fromArrays:()=>ZY,fromAsyncIterable:()=>eQ,fromChannel:()=>A,fromEffect:()=>Sg,fromEffectDrain:()=>Z2,fromEffectRepeat:()=>Y2,fromEffectSchedule:()=>$Y,fromEventListener:()=>nQ,fromIterable:()=>oc,fromIterableEffect:()=>KY,fromIterableEffectRepeat:()=>GY,fromIteratorSucceed:()=>JY,fromPubSub:()=>QY,fromPubSubTake:()=>eL,fromPull:()=>rs,fromQueue:()=>YY,fromReadableStream:()=>XY,fromSchedule:()=>tQ,fromSubscription:()=>rQ,groupAdjacentBy:()=>W9,groupBy:()=>q9,groupByKey:()=>H9,grouped:()=>j9,groupedWithin:()=>z9,haltWhen:()=>f7,ignore:()=>h9,ignoreCause:()=>x9,interleave:()=>c7,interleaveWith:()=>AL,interruptWhen:()=>u7,intersperse:()=>CL,intersperseAffixes:()=>i7,isStream:()=>Ie,iterate:()=>sQ,let:()=>S7,limitBytes:()=>b9,make:()=>jY,map:()=>Jr,mapAccum:()=>P9,mapAccumArray:()=>sc,mapAccumArrayEffect:()=>F9,mapAccumEffect:()=>M9,mapArray:()=>rL,mapArrayEffect:()=>pQ,mapBoth:()=>fQ,mapEffect:()=>Vp,mapError:()=>pL,merge:()=>d0,mergeAll:()=>PQ,mergeEffect:()=>bg,mergeLeft:()=>_Q,mergeResult:()=>OQ,mergeRight:()=>RQ,mkArrayBuffer:()=>F7,mkString:()=>M7,mkUint8Array:()=>N7,never:()=>iQ,onEnd:()=>p7,onError:()=>l7,onExit:()=>kL,onFirst:()=>OL,onStart:()=>vL,orDie:()=>m9,orElseIfEmpty:()=>p9,orElseSucceed:()=>d9,paginate:()=>oQ,partition:()=>YQ,partitionEffect:()=>ZQ,partitionQueue:()=>x0,peel:()=>XQ,pipeThrough:()=>X9,pipeThroughChannel:()=>Y9,pipeThroughChannelOrFail:()=>Q9,prepend:()=>vQ,provide:()=>_L,provideContext:()=>m7,provideService:()=>h7,provideServiceEffect:()=>E0,race:()=>WQ,raceAll:()=>cL,range:()=>aQ,rechunk:()=>SL,repeat:()=>bQ,repeatElements:()=>TQ,result:()=>dQ,retry:()=>g9,run:()=>T7,runCollect:()=>PL,runCount:()=>w7,runDrain:()=>Ig,runFold:()=>A7,runFoldEffect:()=>k7,runForEach:()=>_7,runForEachArray:()=>ML,runForEachWhile:()=>R7,runHead:()=>v7,runIntoPubSub:()=>B7,runIntoQueue:()=>q7,runLast:()=>O7,runSum:()=>C7,scan:()=>N9,scanEffect:()=>U9,schedule:()=>EQ,scoped:()=>cQ,service:()=>DY,serviceOption:()=>LY,share:()=>Z9,sliding:()=>v9,slidingSize:()=>bL,split:()=>O9,splitLines:()=>a7,succeed:()=>rc,suspend:()=>Wr,switchMap:()=>xQ,sync:()=>zY,take:()=>S9,takeRight:()=>E9,takeUntil:()=>dL,takeUntilEffect:()=>mL,takeWhile:()=>hL,takeWhileEffect:()=>T9,takeWhileFilter:()=>I9,tap:()=>nL,tapBoth:()=>mQ,tapCause:()=>fL,tapDefect:()=>o9,tapError:()=>g0,tapErrorTag:()=>n9,tapSink:()=>hQ,throttle:()=>B9,throttleEffect:()=>EL,tick:()=>BY,timeout:()=>IQ,timeoutOrElse:()=>sL,toAsyncIterable:()=>$7,toAsyncIterableEffect:()=>L7,toAsyncIterableWith:()=>T0,toChannel:()=>Un,toPubSub:()=>j7,toPubSubTake:()=>FL,toPull:()=>P7,toQueue:()=>z7,toReadableStream:()=>U7,toReadableStreamEffect:()=>D7,toReadableStreamWith:()=>I0,transduce:()=>J9,transformPull:()=>Dt,transformPullBracket:()=>p0,unfold:()=>tL,unwrap:()=>Aa,unwrapReason:()=>u9,updateContext:()=>RL,updateService:()=>x7,when:()=>QQ,withExecutionPlan:()=>y9,withSpan:()=>g7,zip:()=>NQ,zipFlatten:()=>LQ,zipLatest:()=>qQ,zipLatestAll:()=>h0,zipLatestWith:()=>HQ,zipLeft:()=>UQ,zipRight:()=>DQ,zipWith:()=>m0,zipWithArray:()=>Eg,zipWithIndex:()=>$Q,zipWithNext:()=>BQ,zipWithPrevious:()=>jQ,zipWithPreviousAndNext:()=>zQ});var D2=\"~effect/RcMap\",OY=e=>({[D2]:D2,lookup:e.lookup,context:e.context,scope:e.scope,idleTimeToLive:e.idleTimeToLive,capacity:e.capacity,state:{_tag:\"Open\",map:Up()},pipe(){return Z(this,arguments)}}),L2=e=>cn(t=>{let r=t.context,n=Fe(r,Or),o=OY({lookup:e.lookup,context:r,scope:n,idleTimeToLive:typeof e.idleTimeToLive==\"function\"?rC(e.idleTimeToLive,Ge):je(Ge(e.idleTimeToLive??Ws)),capacity:Math.max(e.capacity??Number.POSITIVE_INFINITY,0)});return Tt(Lo(n,()=>{if(o.state._tag===\"Closed\")return Q;let s=o.state.map;return o.state={_tag:\"Closed\"},sa(s,([,a])=>vn(Wp(a))).pipe(Rr(()=>X(()=>{Ux(s)})))}),o)}),u0=l(2,(e,t)=>Ho(r=>{if(e.state._tag===\"Closed\")return _n;let n=e.state,o=Zu(),s=Xo(n.map,t),a;if(s._tag===\"Some\")a=s.value,a.refCount++;else{if(Number.isFinite(e.capacity)&&WD(e.state.map)>=e.capacity)return P(new fb(`RcMap attempted to exceed capacity of ${e.capacity}`));{a={deferred:ui(),scope:kn(),idleTimeToLive:e.idleTimeToLive(t),finalizer:void 0,fiber:void 0,expiresAt:0,refCount:1},a.finalizer=RY(e,t,a),Dp(n.map,t,a);let c=new Map(e.context.mapUnsafe);o.context.mapUnsafe.forEach((f,p)=>{c.set(p,f)}),c.set(Or.key,a.scope);let u=Oo(c);$(()=>e.lookup(t)).pipe(Rn(u),Ia(a.scope)).addObserver(f=>{db(f)&&_Y(e,t,a,u),Do(a.deferred,f)})}}let i=_o(o.context,Or);return Kt(i,a.finalizer).pipe(_r(r(Fm(a.deferred))))}));var _Y=(e,t,r,n)=>{if(e.state._tag===\"Closed\")return;let o=Xo(e.state.map,t);o._tag===\"None\"||o.value!==r||(Vu(e.state.map,t),!(r.refCount>0)&&Wp(r).pipe(Rn(n),Ia(e.scope)))},Wp=e=>e.fiber?mo(e.fiber).pipe(_r(Le(e.scope,xt))):Le(e.scope,xt),RY=(e,t,r)=>cn(n=>{if(r.refCount--,r.refCount>0)return Q;if(e.state._tag===\"Closed\")return Wp(r);let o=Xo(e.state.map,t);if(o._tag===\"None\"||o.value!==r)return Wp(r);if(vc(r.idleTimeToLive))return Vu(e.state.map,t),Wp(r);if(!Ks(r.idleTimeToLive))return Q;let s=n.getRef(Hc);return r.expiresAt=s.currentTimeMillisUnsafe()+Cr(r.idleTimeToLive),r.fiber?Q:(r.fiber=Ho(function a(i){let c=s.currentTimeMillisUnsafe(),u=r.expiresAt-c;if(u<=0){if(e.state._tag===\"Closed\"||r.refCount>0)return Q;let f=Xo(e.state.map,t);return f._tag===\"None\"||f.value!==r?Q:(Vu(e.state.map,t),Le(r.scope,xt))}return I(i(s.sleep(Js(u))),()=>a(i))}).pipe(yi(X(()=>{r.fiber=void 0})),Rn(n.context),Ia(e.scope)),Q)});var f0=l(2,(e,t)=>Si(r=>{if(e.state._tag===\"Closed\")return Q;let n=Xo(e.state.map,t);if(n._tag===\"None\"||vc(n.value.idleTimeToLive))return Q;let o=n.value;return o.expiresAt=r.currentTimeMillisUnsafe()+Cr(o.idleTimeToLive),Q}));var PY=\"~effect/RcRef\",Jp={_tag:\"Empty\"},MY={_tag:\"Closed\"},FY={_A:R,_E:R},l0=class{[PY]=FY;pipe(){return Z(this,arguments)}state=Jp;acquire;context;scope;idleTimeToLive;constructor(t,r,n,o){this.acquire=t,this.context=r,this.scope=n,this.idleTimeToLive=o}},j2=e=>cn(t=>{let r=t.context,n=Fe(r,Or),o=new l0(e.acquire,r,n,e.idleTimeToLive!==void 0?Ge(e.idleTimeToLive):void 0);return Tt(Lo(n,()=>{let s=o.state;return o.state=MY,s._tag===\"Acquired\"?Le(s.scope,xt):s._tag===\"Acquiring\"?mo(s.fiber):Q}),o)}),NY=(e,t)=>{switch(e.state._tag){case\"Closed\":return _n;case\"Acquired\":{let r=e.state;return r.refCount++,r.fiber?Tt(mo(r.fiber),r):g(r)}case\"Acquiring\":{let r=e.state;return r.awaiters++,B2(e,r,t)}case\"Empty\":{let r={_tag:\"Acquiring\",fiber:void 0,awaiters:1,acquired:void 0};e.state=r;let n=kn(),o=On(e.acquire,Jt(e.context,Or,n)).pipe(I(s=>{if(e.state!==r)return _n;let a={_tag:\"Acquired\",value:s,scope:n,fiber:void 0,refCount:r.awaiters,invalidated:!1};return e.state=a,r.acquired=a,g(a)}),un(s=>Ve(s)?Q:(e.state===r&&(e.state=Jp),Le(n,s))));return r.fiber=Rn(Zu().context)(o),B2(e,r,t)}}},B2=(e,t,r)=>un(r(Ea(t.fiber)),n=>{let o=t.acquired;return o!==void 0?Uo(n)?q2(e,o):Q:--t.awaiters>0||e.state!==t?Q:(e.state=Jp,mo(t.fiber))}),z2=e=>{let t=e;return Ho(r=>I(xi,n=>I(NY(t,r),o=>Tt(Lo(n,()=>q2(t,o)),o.value))))},q2=(e,t)=>{if(t.refCount--,t.refCount>0)return Q;let r=e.idleTimeToLive;return r===void 0||t.invalidated?(e.state===t&&(e.state=Jp),Le(t.scope,xt)):Ks(r)?(t.fiber=Ho(n=>I(n(ua(r)),()=>e.state===t&&t.refCount===0?(e.state=Jp,Le(t.scope,xt)):Q)).pipe(yi(X(()=>{t.fiber=void 0})),Rn(e.context),Ia(e.scope)),Q):Q};var H2=j2,W2=z2;var G2=\"~effect/Stream\",Ie=e=>k(e,G2),V2=zp,A=M2,Sg=e=>A(fg(j(e,Ee))),DY=e=>Sg(Yb(e)),LY=e=>Sg(Qb(e)),Z2=e=>rs(g(I(e,()=>Y()))),Y2=e=>rs(g(j(e,Ee))),$Y=(e,t)=>rs(dr(function*(){let r=yield*Kn(t),n=yield*qo(e,Gt,Gt.defaultValue()),o=!0,s=$(()=>r(n)).pipe(I(a=>qo(e,Gt,a)),j(a=>(n=a,Ee(a))));return $(()=>o?(o=!1,g(Ee(n))):s)})),BY=e=>rs(X(()=>{let t=!0,r=g(Ee(void 0)),n=Jb(r,e);return $(()=>t?(t=!1,r):n)})),rs=e=>A(er(e)),Dt=(e,t)=>A(Ce((r,n)=>I(ts(e.channel,n),o=>t(o,n)))),p0=(e,t)=>A(go((r,n,o)=>I(ts(e.channel,n),s=>t(s,n,o)))),Un=e=>e.channel,Q2=(e,t)=>A(S1(e,t)),Ns=A(Xu),rc=e=>A(ug(Ee(e))),jY=(...e)=>Gp(e),zY=e=>A(I1(()=>Ee(e()))),Wr=e=>A(cg(()=>e().channel)),nf=e=>A(es(e)),qY=e=>A(w1(e)),X2=e=>A(qp(e)),HY=e=>A(Gw(e)),WY=e=>A(C1(e)),JY=(e,t)=>A(Kw(()=>e,t)),oc=(e,t)=>Array.isArray(e)&&t?.chunkSize===void 0?Gp(e):A(E1(e,t?.chunkSize)),KY=e=>Aa(j(e,oc)),GY=e=>of(Y2(e),oc),Gp=e=>tt(e)?A(ug(e)):Ns,VY=e=>Aa(j(e,Gp)),ZY=(...e)=>A(Jw(jd(e,tt))),YY=e=>A(k1(e)),QY=e=>A(v1(e)),eL=e=>A(O1(e)),XY=e=>A(_1(e)),eQ=(e,t)=>A(R1(e,t)),tQ=e=>rs(j(Cl(e),t=>He(j(t(void 0),Ee),()=>Y()))),rQ=e=>A(Zw(e)),nQ=(e,t,r)=>Q2(n=>{let o=typeof r==\"object\"&&r.once;function s(a){rg(n,a),o&&og(n)}return gi(X(()=>e.addEventListener(t,s,r)),()=>X(()=>e.removeEventListener(t,s,r)))},{bufferSize:typeof r==\"object\"?r.bufferSize:void 0}),tL=(e,t)=>rs(X(()=>{let r=e;return I($(()=>t(r)),n=>n===void 0?Y():(r=n[1],g(Ee(n[0]))))})),oQ=(e,t)=>rs(X(()=>{let r=e,n=!1;return $(function o(){return n?Y():I(t(r),([s,a])=>(fe(a)?n=!0:r=a.value,tt(s)?g(s):o()))})})),sQ=(e,t)=>tL(e,r=>g([r,t(r)])),aQ=(e,t,r=zp)=>e>t?Ns:rs(X(()=>{let n=Tn(r),o=e,s=!1;return $(()=>{if(s)return Y();let a=t-o+1;if(a>n){let c=xy(o,o+n-1);return o+=n,g(c)}let i=xy(o,o+a-1);return s=!0,g(i)})})),iQ=A(T1),Aa=e=>A(Ca(j(e,Un))),cQ=e=>A(l2(e.channel)),Jr=l(2,(e,t)=>Wr(()=>{let r=0;return A(yo(e.channel,wn(n=>t(n,r++))))})),uQ=l(2,(e,t)=>Jr(e,()=>t)),fQ=l(2,(e,t)=>e.pipe(Jr(t.onElement),pL(t.onError))),rL=l(2,(e,t)=>A(yo(e.channel,t))),Vp=l(e=>Ie(e[0]),(e,t,r)=>e.channel.pipe(gr,lg(t,r),yo(Ee),A)),lQ=l(e=>Ie(e[0]),(e,t)=>Vp(e,R,t)),pQ=l(2,(e,t)=>A(lg(e.channel,t))),dQ=e=>e.pipe(Jr(B),Zp(t=>rc(W(t)))),nL=l(e=>Ie(e[0]),(e,t,r)=>Vp(e,n=>Tt(t(n),n),r)),mQ=l(2,(e,t)=>e.pipe(g0(t.onError),nL(t.onElement,{concurrency:t.concurrency}))),hQ=l(2,(e,t)=>p0(e,Se(function*(r,n,o){let s=Pr(),a=Pr(),i,c,u=!1,f=!1,p=s.whenOpen($(()=>{if(i){let m=i;return i=void 0,f||s.closeUnsafe(),Tt(a.open,m)}return Y()}));yield*$(()=>t.transform(p,o)).pipe(m=>Fl(m,x=>(u=!0,Uo(x)&&(c=x.cause),a.open),!0),Ut(o));let d=r.pipe(I(m=>c?ze(c):u?g(m):(i=m,a.closeUnsafe(),s.openUnsafe(),Tt(a.await,m))),He(()=>c?ze(c):u?Y():(f=!0,a.closeUnsafe(),s.openUnsafe(),I(a.await,()=>c?ze(c):Y()))));return $(()=>c?ze(c):u?r:d)}))),of=l(e=>Ie(e[0]),(e,t,r)=>e.channel.pipe(gr,Yw(n=>t(n).channel,r),A)),xQ=l(e=>Ie(e[0]),(e,t,r)=>e.channel.pipe(gr,c2(n=>t(n).channel,r),A)),oL=l(e=>Ie(e[0]),(e,t)=>of(e,R,t)),gQ=e=>A(gr(e.channel)),yQ=e=>A($1(e.channel)),SQ=l(2,(e,t)=>bg(e,Ig(t))),bQ=l(2,(e,t)=>A(B1(e.channel,t))),EQ=l(2,(e,t)=>e.channel.pipe(gr,q1(t),yo(Ee),A)),IQ=l(2,(e,t)=>sL(e,{duration:t,orElse:()=>Ns})),sL=l(2,(e,t)=>{let r=Ge(t.duration);if(!Ks(r))return e;if(vc(r))return Wr(t.orElse);let n=Symbol();return uL(Wr(()=>{let s=Zu().getRef(Hc),a=Cr(r),i,c=Pr(!1);return d0(Dt(e,(u,f)=>$(()=>(i=s.currentTimeMillisUnsafe()+a,c.openUnsafe(),u)).pipe(j(p=>(c.closeUnsafe(),i=void 0,p)),g)),Z2(dr(function*(){for(;;)if(yield*c.await,!(i===void 0||(yield*ua(i-s.currentTimeMillisUnsafe()),i===void 0)||i-s.currentTimeMillisUnsafe()>0))return yield*an(n)})),{haltStrategy:\"left\"})}),o=>o.reasons.find(a=>a._tag===\"Die\"&&a.defect===n)?t.orElse():X2(o))}),TQ=l(2,(e,t)=>A(Ce((r,n)=>j(he(gr(e.channel))(r,n),o=>{let s,a=dr(function*(){let i=yield*o,c=Ee(i);return s=(yield*Cl(t))(i).pipe(Tt(c),He(f=>(s=void 0,a))),c});return $(()=>s??a)})))),wQ=e=>A(z1(e.channel)),CQ=e=>of(e,oc),AQ=e=>e.channel.pipe(gr,L1,A),nc=l(2,(e,t)=>{let r=A(D1(b1(()=>kQ(e,t))));return aL.set(r,[e,t]),r}),aL=new WeakMap;function*kQ(e,t){let r=[t,e];for(;r.length>0;){let n=r.pop(),o=aL.get(n);o===void 0?yield n.channel:r.push(o[1],o[0])}}var vQ=l(2,(e,t)=>nc(oc(t),e)),d0=l(e=>Ie(e[0])&&Ie(e[1]),(e,t,r)=>A(hg(Un(e),Un(t),r))),bg=l(2,(e,t)=>e.channel.pipe(u2(t),A)),OQ=l(2,(e,t)=>d0(Jr(e,B),Jr(t,W))),_Q=l(2,(e,t)=>bg(e,Ig(t))),RQ=l(2,(e,t)=>bg(t,Ig(e))),PQ=l(2,(e,t)=>oL(oc(e),t)),MQ=l(2,(e,t)=>iL(e,t,(r,n)=>[r,n])),iL=l(3,(e,t,r)=>of(e,n=>Jr(t,o=>r(n,o)))),m0=l(3,(e,t,r)=>Eg(e,t,FQ(r))),FQ=e=>(t,r)=>{let n=Math.min(t.length,r.length),o=[];for(let s=0;sA(go(Se(function*(n,o){let s=yield*ts(e.channel,o),a=yield*ts(t.channel,o),i=dr(function*(){let f=yield*Ut(s,o),p=yield*Ut(a,o);return yield*KD([f,p])}),c={_tag:\"PullBoth\"};return dr(function*(){let[f,p]=c._tag===\"PullBoth\"?yield*i:c._tag===\"PullLeft\"?[yield*s,c.rightArray]:[c.leftArray,yield*a],d=r(f,p);return tt(d[1])?c={_tag:\"PullRight\",leftArray:d[1]}:tt(d[2])?c={_tag:\"PullLeft\",rightArray:d[2]}:c={_tag:\"PullBoth\"},d[0]})})))),NQ=l(2,(e,t)=>m0(e,t,(r,n)=>[r,n])),UQ=l(2,(e,t)=>Eg(e,t,(r,n)=>{let o=Math.min(r.length,n.length),s=r.slice(0,o),a=r.slice(o),i=n.slice(o);return[s,a,i]})),DQ=l(2,(e,t)=>Eg(e,t,(r,n)=>{let o=Math.min(r.length,n.length),s=n.slice(0,o),a=r.slice(o),i=n.slice(o);return[s,a,i]})),LQ=l(2,(e,t)=>m0(e,t,(r,n)=>[...r,n])),$Q=e=>Jr(e,(t,r)=>[t,r]),BQ=e=>sc(e,v,(t,r)=>{let n=0;t._tag===\"None\"&&(n=1,t=O(r[0]));let o=Pt();for(;nsc(e,v,(t,r)=>{let n=Pt();for(let o=0;osc(e,()=>({prev:v(),current:v()}),(t,r)=>{let n=0,o;t.current._tag===\"None\"?(n=1,o=r[0],t.current=O(o)):o=t.current.value;let s=Pt();for(;nA(cg(()=>{let t=[],r=new Set,n=Pr(),o=Pr();return mg(Jw(e.map((s,a)=>s.channel.pipe(gr,lg(i=>(t[a]=i,r.has(a)?g(Ee(t.slice())):(r.add(a),r.sizer.has(a)?Q:o.open)))),{concurrency:\"unbounded\",bufferSize:0}).pipe(t0(o.await))})),qQ=l(2,(e,t)=>h0(e,t)),HQ=l(3,(e,t,r)=>Jr(h0(e,t),([n,o])=>r(n,o))),cL=(...e)=>A(Ce((t,r)=>X(()=>{let n,o=Kb(e.map(s=>{let a=Nt(r);return ts(s.channel,a).pipe(I(i=>Hb(g(i),i)),un(i=>i._tag===\"Success\"?n?Le(a,i):(n=i.value[0],Q):Le(a,i)),j(([,i])=>i))}));return $(()=>n??o)}))),WQ=l(2,(e,t)=>cL(e,t)),JQ=l(2,(e,t)=>A(W1(Un(e),t))),KQ=l(2,(e,t)=>A(J1(Un(e),t))),GQ=l(2,(e,t)=>A(K1(Un(e),t))),VQ=l(2,(e,t)=>A(G1(Un(e),t))),x0=l(e=>Ie(e[0]),Se(function*(e,t,r){let n=yield*xi,o=yield*ts(e.channel,n),s=r?.capacity===\"unbounded\"?void 0:r?.capacity??V2,a=yield*Mn({capacity:s}),i=yield*Mn({capacity:s});return yield*dr(function*(){for(;;){let c=yield*o,u=[],f=[];for(let d=0;d0){let d=ng(a,f);d.length>0&&(p=yield*eu(Yi(a,d)))}if(u.length>0){let d=ng(i,u);d.length>0&&(yield*Yi(i,d))}p&&(yield*Ea(p))}}).pipe(Vn(c=>(wa(a,c),wa(i,c),Q)),Ut(n)),[a,i]})),Kp=e=>A(Ce((t,r)=>Tt(Kt(r,Fn(e)),Qi(e)))),ZQ=l(e=>Ie(e[0]),(e,t,r)=>j(x0(Vp(e,n=>t(n),r),n=>n,r),([n,o])=>[Kp(n),Kp(o)])),YQ=l(e=>Ie(e[0]),(e,t,r)=>j(x0(e,t,{capacity:r?.capacity??16}),([n,o])=>[Kp(n),Kp(o)])),QQ=l(2,(e,t)=>t.pipe(j(r=>r?e:Ns),Aa)),XQ=l(2,Se(function*(e,t){let r,n=yield*s0(e.channel),o=rt(n,c=>(r=c,ze(c))),[s,a]=yield*sh(c=>t.transform(o,c)),i=r?Ns:rs(g(n));return a&&(i=nc(Gp(a),i)),[s,i]})),e9=l(2,(e,t)=>A(d2(e.channel,t))),t9=l(2,(e,t)=>A(p2(e.channel,t))),uL=l(2,(e,t)=>e.channel.pipe(tf(r=>t(r).channel),A)),r9=l(2,(e,t)=>e.channel.pipe(Z1(r=>t(r).channel),A)),fL=l(2,(e,t)=>e.channel.pipe(pg(t),A)),Zp=l(2,(e,t)=>A(xo(e.channel,r=>t(r).channel)));var g0=l(2,(e,t)=>e.channel.pipe(Q1(t),A)),n9=l(3,(e,t,r)=>{let n=Array.isArray(t)?o=>k(o,\"_tag\")&&t.includes(o._tag):At(t);return g0(e,o=>n(o)?r(o):Q)}),o9=l(2,(e,t)=>fL(e,r=>{let n=dl(r);return mt(n)?t(n.success):Q})),lL=l(e=>Ie(e[0]),(e,t,r,n)=>A(X1(Un(e),t,o=>r(o).channel,n&&(o=>n(o).channel)))),y0=l(e=>Ie(e[0]),(e,t,r,n)=>A(e2(Un(e),t,o=>r(o).channel,n&&(o=>n(o).channel)))),s9=l(e=>Ie(e[0]),(e,t,r,n)=>{let o=Array.isArray(t)?s=>k(s,\"_tag\")&&t.includes(s._tag):At(t);return lL(e,o,r,n)}),a9=l(e=>Ie(e[0]),(e,t,r)=>{let n;return y0(e,o=>(n??=Object.keys(t),k(o,\"_tag\")&&Hm(o._tag)&&n.includes(o._tag)?B(o):W(o)),o=>t[o._tag](o),r)}),i9=l(e=>Ie(e[0]),(e,t,r,n,o)=>A(t2(Un(e),t,r,(s,a)=>n(s,a).channel,o&&((s,a)=>o(s,a).channel)))),c9=l(e=>Ie(e[0]),(e,t,r,n)=>{let o=Object.create(null);for(let a of Object.keys(r)){let i=r[a];o[a]=(c,u)=>i(c,u).channel}let s=n&&((a,i)=>n(a,i).channel);return A(r2(e.channel,t,o,s))}),u9=l(2,(e,t)=>y0(e,r=>At(r,t)&&k(r,\"reason\")?B(r.reason):W(r),nf)),pL=l(2,(e,t)=>A(n2(e.channel,t))),f9=l(3,(e,t,r)=>A(Y1(e.channel,t,n=>r(n).channel))),l9=l(3,(e,t,r)=>A(dg(e.channel,t,(n,o)=>r(n,o).channel))),p9=l(2,(e,t)=>A(U1(e.channel,r=>Un(t())))),d9=l(2,(e,t)=>Zp(e,r=>rc(t(r)))),m9=e=>A(o2(e.channel)),h9=l(e=>Ie(e[0]),(e,t)=>A(s2(e.channel,t))),x9=l(e=>Ie(e[0]),(e,t)=>A(a2(e.channel,t))),g9=l(2,(e,t)=>A(i2(e.channel,t))),K2=(e,t)=>Aa(j(Kn(t),r=>{let n=Gt.defaultValue(),o=()=>Zp(E0(e,Gt,X(()=>n)),s=>Aa(He(j(r(s),a=>(n=a,Aa(Tt(Rl,o())))),()=>g(nf(s)))));return o()})),y9=l(e=>Ie(e[0]),(e,t,r)=>Wr(()=>{let n=r?.preventFallbackOnPartialStream??!1,o=0,s={attempt:0,stepIndex:0},a=E0(Gm,X(()=>(s={attempt:s.attempt+1,stepIndex:o},s))),i=r?.onEvent===void 0?void 0:$b(r.onEvent,()=>s),c,u=i===void 0?R:d=>kL(vL(d,j(i.begin,m=>{c=m})),m=>$(()=>{if(c===void 0)return Q;let x=c;return c=void 0,i.end(x,m)})),f=v(),p=Wr(()=>{let d=t.steps[o];if(!d)return nf(uy(f));let m=a(u(_L(e,d.provide))),x=!1;if(be(f)){let y=f.value,S=!1,w=m;m=Wr(()=>S?w:(S=!0,nf(y))),m=K2(m,_l(d,!1))}else{let y=_l(d,!0);m=y?K2(m,y):m}return Zp(n?OL(m,y=>(x=!0,Q)):m,y=>(o++,n&&x?nf(y):(f=O(y),p)))});return p})),S9=l(2,(e,t)=>{let r=ot(t);return r===0?Ns:dL(e,(n,o)=>o===r-1)}),b9=l(3,(e,t,r)=>Wr(()=>{let n=ch(t),o=BigInt(0),s=!1;return nc(hL(e,a=>{let i=o+BigInt(a.length);return i>n?(s=!0,!1):(o=i,!0)}),Wr(()=>s?r():Ns))})),E9=l(2,(e,t)=>{let r=ot(t);return r===0?Ns:sc(e,Ps,(n,o)=>($x(n,o),n.length>r&&jx(n,n.length-r),[n,Fs]),{onHalt(n){return Ta(n)}})}),dL=l(e=>Ie(e[0]),(e,t,r)=>Dt(e,(n,o)=>X(()=>{let s=0,a=!1;return I($(()=>a?Y():n),c=>{let u=c.findIndex(f=>t(f,s++));if(u>=0){a=!0;let f=c.slice(0,r?.excludeLast?u:u+1);return tt(f)?g(f):Y()}return g(c)})}))),mL=l(e=>Ie(e[0]),(e,t,r)=>Dt(e,(n,o)=>X(()=>{let s=0,a=!1;return dr(function*(){if(a)return yield*Y();let i=yield*n;for(let c=0;cDt(e,(r,n)=>X(()=>{let o=0,s=!1,a=I($(()=>s?Y():r),i=>{let c=[];for(let u=0;uDt(e,(r,n)=>X(()=>{let o=!1,s=I($(()=>o?Y():r),a=>{let i=[];for(let c=0;cmL(e,(r,n)=>j(t(r,n),o=>!o),{excludeLast:!0})),S0=l(2,(e,t)=>{let r=ot(t);return r===0?e:Dt(e,(n,o)=>X(()=>{let s=0,a=n.pipe(I(i=>s>=r?g(i):(s+=i.length,s<=r?a:g(i.slice(r-s)))));return a}))}),w9=l(2,(e,t)=>S0(xL(e,(r,n)=>!t(r,n)),1)),C9=l(2,(e,t)=>S0(gL(e,(r,n)=>j(t(r,n),o=>!o)),1)),xL=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o=!0,s=0,a=I(r,i=>{let c=i.findIndex(u=>!t(u,s++));return c===-1?a:(o=!1,g(i.slice(c)))});return $(()=>o?a:r)}))),A9=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o=!0,s=I(r,a=>{let i=a.findIndex(c=>ce(t(c)));return i===-1?s:(o=!1,g(a.slice(i)))});return $(()=>o?s:r)}))),gL=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o=!0,s=0,a=dr(function*(){for(;;){let i=yield*r;for(let c=0;co?a:r)}))),k9=l(2,(e,t)=>{let r=ot(t);return r===0?e:Dt(e,(n,o)=>X(()=>{let s=Ps(),a=I(n,i=>{Bx(s,i);let c=s.length-r,u=Vi(s,c);return or(u)?g(u):a});return a}))}),yL=e=>e.channel.pipe(yo(Ee),A),SL=l(2,(e,t)=>(t=Tn(t),Dt(e,(r,n)=>X(()=>{let o=Pt(),s=0,a,i=!1;return $(function c(){if(i)return Y();if(a===void 0)return I(r,u=>{if(o.length===0&&u.length===t)return g(u);if(o.length+u.length{if(o.length===0)return Y();let c=o;return i=!0,o=[],g(c)}))})))),v9=l(2,(e,t)=>bL(e,t,1)),bL=l(3,(e,t,r)=>{let n=Tn(t),o=Tn(r);return Dt(e,(s,a)=>X(()=>{let i=null,c=Ps(),u=!1,f=0,p=zo(s,{onSuccess(d){if(Bx(c,d),f>0){let x=c.length;jx(c,f),f=Math.max(0,f-x)}if(c.length=n;)if(n===o)m.push(Vi(c,n));else{m.push(QD(c,n));let x=c.length;jx(c,o),f=Math.max(0,o-x)}return g(m)},onFailure(d){return c.length===0||u&&c.length<=n-o?ze(d):(i=d,g(Ee(Ta(c))))}});return $(()=>i?ze(i):p)}))}),O9=l(2,(e,t)=>sc(e,Pt,(r,n)=>{let o=Pt();for(let s=0;sQw(gr(e.channel),gr(t.channel),r,n).pipe(yo(Ee),A)),R9=l(4,(e,t,r,n)=>A(Qw(e.channel,t.channel,r,n))),P9=l(e=>Ie(e[0]),(e,t,r,n)=>A(ef(e.channel,t,(o,s)=>{let a=Pt();for(let i=0;iIe(e[0]),(e,t,r,n)=>A(ef(e.channel,t,(o,s)=>{let[a,i]=r(o,s);return o=a,[o,tt(i)?Ee(i):Fs]},n?.onHalt?{onHalt(o){let s=n.onHalt(o);return tt(s)?Ee(s):Fs}}:void 0))),Fs=Pt(),M9=l(e=>Ie(e[0]),(e,t,r,n)=>e.channel.pipe(gr,ef(t,(o,s)=>j(r(o,s),([a,i])=>[a,tt(i)?Ee(i):Pt()]),n?.onHalt?{onHalt(o){let s=n.onHalt(o);return tt(s)?Ee(s):Fs}}:void 0),A)),F9=l(e=>Ie(e[0]),(e,t,r,n)=>e.channel.pipe(ef(t,(o,s)=>j(r(o,s),([a,i])=>[a,tt(i)?Ee(i):Fs]),n?.onHalt?{onHalt(o){let s=n.onHalt(o);return tt(s)?Ee(s):Fs}}:void 0),A)),N9=l(3,(e,t,r)=>Wr(()=>{let n=t();return nc(rc(n),A(ef(e.channel,()=>n,(o,s)=>{let a=Pt();for(let i=0;iWr(()=>e.channel.pipe(gr,V1(t(),r),yo(Ee),A))),D9=l(2,(e,t)=>Dt(e,Se(function*(r,n){let o=yield*Hc,s=Cr(Ge(t)),a,i,c=1/0,u=Pr(),f=Pr(),p=Pr();yield*r.pipe(u.whenOpen,I(m=>(f.openUnsafe(),a=m,c=o.currentTimeMillisUnsafe()+s,Q)),$t({disableYield:!0}),Vn(m=>(i=m,c=o.currentTimeMillisUnsafe(),f.openUnsafe(),p.openUnsafe(),Q)),Ut(n));let d=$(function m(){let x=o.currentTimeMillisUnsafe(),y=c{if(o.currentTimeMillisUnsafe(){if(i){if(a){let m=g(Ee(Bd(a)));return a=void 0,m}return ze(i)}return u.openUnsafe(),f.whenOpen(d)})}))),EL=l(2,(e,t)=>{let r=t.burst??0;return t.strategy===\"enforce\"?L9(e,t.cost,t.units,t.duration,r):$9(e,t.cost,t.units,t.duration,r)}),L9=(e,t,r,n,o)=>Dt(e,s=>Si(a=>{let i=Cr(Ge(n)),c=r+o<0?Number.POSITIVE_INFINITY:r+o,u=r,f=a.currentTimeMillisUnsafe();return g(I(s,function p(d){return I(t(d),m=>{let x=a.currentTimeMillisUnsafe(),S=(x-f)/i,w=u+S*r,E=w<0?c:Math.min(w,c);return m<=E?(u=E-m,f=x,g(d)):I(s,p)})}))})),$9=(e,t,r,n,o)=>Dt(e,s=>Si(a=>{let i=Cr(Ge(n)),c=r+o<0?Number.POSITIVE_INFINITY:r+o,u=r,f=a.monotonicTimeNanosUnsafe();return g(I(s,p=>I(t(p),d=>{let m=a.monotonicTimeNanosUnsafe(),y=Number(m-f)/1e6/i,S=u+y*r,E=Math.min(S,c)-d;if(E>=0)return u=E,f=m,g(p);let U=-E/r,_=Math.max(0,U*i);return _>0?I(ua(_),()=>(u=E,f=m,g(p))):(u=E,f=m,g(p))})))})),B9=l(2,(e,t)=>EL(e,{...t,cost:r=>g(t.cost(r))})),j9=l(2,(e,t)=>yL(SL(e,t))),z9=l(3,(e,t,r)=>b0(e,N2(Tn(t)),Nb(r))),q9=l(e=>Ie(e[0]),(e,t,r)=>IL(e,Se(function*(n,o,s){for(let a=0;aIe(e[0]),(e,t,r)=>Wr(()=>{let n=Up();return IL(e,Se(function*(o,s,a){for(let i=0;ip0(e,Se(function*(n,o,s){let a=yield*o1();yield*Kt(o,Fn(a));let i=Up(),c=yield*L2({lookup:u=>gi(Mn({capacity:r?.bufferSize??4096}).pipe(Rr(f=>(Dp(i,u,f),Sn(a,[u,Kp(f)])))),f=>(Vu(i,u),jw(f))),idleTimeToLive:r?.idleTimeToLive??ls}).pipe(Es(s));return yield*Gn({while:Sr,body:je(I(n,u=>t(u,c,i))),step:$n}).pipe(rt(u=>Bt(a,u)),Ut(o)),Qi(a)})),W9=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o,s,a=Pt(),i=r.pipe(I(u=>{for(let f=0;fc?Y():i),()=>{c=!0;let u=s;return s=void 0,u&&or(u)?g(Ee([o,u])):Y()})}))),J9=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o,s,a=$(()=>{if(s!==void 0){let c=s;return s=void 0,g(c)}return r}).pipe(ca(c=>(o=bs(c),Y()))),i=j($(()=>t.transform(a,n)),([c,u])=>(s=u,Ee(c)));return $(()=>o||i)}))),K9=l(2,(e,t)=>b0(e,t,kl)),b0=l(3,(e,t,r)=>A(go(Se(function*(n,o,s){let a=yield*ts(e.channel,o),i=Pr(!1),c=Symbol(),u=yield*Mn({capacity:0});yield*a.pipe(i.whenOpen,I(h=>(i.closeUnsafe(),Sn(u,h))),$t,rt(h=>Bt(u,h)),Ut(s));let f=yield*Hc,p=yield*di(r),d=Pr(!1),m=f.currentTimeMillisUnsafe(),x=0,y=v(),S,w=h=>(d.openUnsafe()&&(x=f.currentTimeMillisUnsafe()),g(h)),E=d.await.pipe(I(()=>p(m,y)),I(([,h])=>ua(h)),I(()=>Sn(u,c)),He(()=>jw(u)),I(()=>mi)),U=$(()=>{if(S!==void 0){let h=S;return S=void 0,w(h)}return u.state._tag===\"Open\"&&i.openUnsafe(),I(Ms(u),h=>h===c?Y():w(h))}),_=I(([h,b])=>{if(d.isOpen())m+=f.currentTimeMillisUnsafe()-x;else if(u.state._tag===\"Done\")return Y();return y=O(h),S=b,g(Ee(h))});return $(()=>{if(u.state._tag===\"Done\"&&S===void 0)return u.state.exit;d.closeUnsafe();let h=_($(()=>t.transform(U,s)));return u.state._tag===\"Open\"?Xc(h,E):h})})))),G9=l(2,Se(function*(e,t){let r=ot(t.n),n=yield*V9(t),o=new Array(r),s=yield*Or;for(let a=0;aLe(i,u)),A)}return yield*ec(e.channel,a=>Qx(n,a)).pipe(un(a=>Yx(n,a)),tu),o})),V9=e=>gi(e.capacity===\"unbounded\"?Zx(e):e.strategy===\"dropping\"?Gx(e):e.strategy===\"sliding\"?Vx(e):Kx(e),$p),TL=l(2,(e,t)=>j(FL(e,t),eL)),Z9=l(2,(e,t)=>j(H2({acquire:TL(e,t),idleTimeToLive:t.idleTimeToLive}),r=>Aa(W2(r)))),Y9=l(2,(e,t)=>A(Xw(e.channel,t))),Q9=l(2,(e,t)=>A(e0(e.channel,t))),X9=l(2,(e,t)=>e.channel.pipe(e0(F2(t)),N1(([r,n])=>n?ug(n):Xu),A)),e7=e=>Sg(PL(e)),t7=e=>sc(e,Pt,(t,r)=>{let n=gy(t,r);return[n,[n]]}),r7=e=>wL(e,J),wL=l(2,(e,t)=>Dt(e,(r,n)=>X(()=>{let o=!0,s;return I(r,function a(i){let c=[],u=0;for(o&&(o=!1,s=i[0],u=1,c.push(s));uDt(e,(r,n)=>X(()=>{let o=!0,s;return I(r,Se(function*a(i){let c=[],u=0;for(o&&(o=!1,s=i[0],u=1,c.push(s));uIe(e[0]),(e,t)=>Wr(()=>{let r=new TextDecoder(t?.encoding);return Jr(e,n=>r.decode(n,{stream:!0}))})),s7=e=>Wr(()=>{let t=new TextEncoder;return Jr(e,r=>t.encode(r))}),a7=e=>e.channel.pipe(Xw(f2()),A),CL=l(2,(e,t)=>rL(e,(r,n)=>{let o=n===0?[]:[t],s=r.length-1;for(let a=0;arc(t.start).pipe(nc(CL(e,t.middle)),nc(rc(t.end)))),c7=l(2,(e,t)=>AL(e,t,oc(fA([!0,!1])))),AL=l(3,(e,t,r)=>A(Ce(Se(function*(n,o){let s=yield*he(gr(r.channel))(n,o),a=Symbol(),i=!1,c=!1,u=(yield*he(gr(e.channel))(n,o)).pipe(He(()=>(i=!0,g(a)))),f=(yield*he(gr(t.channel))(n,o)).pipe(He(()=>(c=!0,g(a))));return dr(function*(){for(;;){if(i&&c)return yield*Y();let p=yield*s;if(p&&i||!p&&c)continue;let d=yield*p?u:f;if(d!==a)return Ee(d)}})})))),u7=l(2,(e,t)=>A(t0(e.channel,t))),f7=l(2,(e,t)=>A(m2(e.channel,t))),kL=l(2,(e,t)=>A(Hp(e.channel,t))),l7=l(2,(e,t)=>A(h2(e.channel,t))),vL=l(2,(e,t)=>A(x2(e.channel,t))),OL=l(2,(e,t)=>A(r0(e.channel,r=>t(r[0])))),p7=l(2,(e,t)=>A(g2(e.channel,t))),d7=l(2,(e,t)=>A(y2(e.channel,t))),_L=l(e=>Ie(e[0]),(e,t,r)=>A(S2(e.channel,t,r))),m7=l(2,(e,t)=>A(xg(e.channel,t))),h7=l(3,(e,t,r)=>A(gg(e.channel,t,r))),E0=l(3,(e,t,r)=>A(yg(e.channel,t,r))),RL=l(2,(e,t)=>A(b2(e.channel,t))),x7=l(3,(e,t,r)=>RL(e,n=>Jt(n,t,r(Fe(n,t))))),g7=function(){let e=Ie(arguments[0]),t=e?arguments[1]:arguments[0],r=Gs(e?arguments[2]:arguments[1]);if(e){let n=arguments[0];return A(n0(n.channel,t,r))}return n=>A(n0(n.channel,t,r))},y7=rc({}),S7=l(3,(e,t,r)=>Jr(e,n=>({...n,[t]:r(n)})));var b7=l(e=>Ie(e[0]),(e,t,r,n)=>of(e,o=>Jr(r(o),s=>({...o,[t]:s})),n)),E7=l(e=>Ie(e[0]),(e,t,r,n)=>Vp(e,o=>j(r(o),s=>({...o,[t]:s})),n)),I7=l(2,(e,t)=>Jr(e,r=>({[t]:r}))),T7=l(2,(e,t)=>sh(r=>ts(e.channel,r).pipe(I(n=>t.transform(n,r)),j(([n])=>n)))),PL=e=>tc(e.channel,()=>[],(t,r)=>{for(let n=0;ntc(e.channel,()=>0,(t,r)=>t+r.length),C7=e=>tc(e.channel,()=>0,(t,r)=>{for(let n=0;ntc(e.channel,t,(n,o)=>{for(let s=0;sC2(e.channel,t,(n,o)=>{let s=0,a=n;return j(Gn({while:()=>sr(a,o[s]),step(i){a=i,s++}}),()=>a)})),v7=e=>j(T2(e.channel),Rt(yA(0))),O7=e=>j(w2(e.channel),Rt(Bd)),_7=l(2,(e,t)=>ec(e.channel,r=>{let n=0;return Gn({while:()=>nt(r[n++]),step:$n})})),R7=l(2,(e,t)=>I2(e.channel,r=>{let n=!1,o=0;return j(Gn({while:()=>!n&&ot(r[o]),step(s){o++,s||(n=!0)}}),()=>!n)})),ML=l(2,(e,t)=>ec(e.channel,t)),Ig=e=>E2(e.channel),P7=e=>s0(e.channel),M7=e=>tc(e.channel,()=>\"\",(t,r)=>t+r.join(\"\")),F7=e=>j(o0(e.channel),t=>t.buffer),N7=e=>o0(e.channel),I0=l(e=>Ie(e[0]),(e,t,r)=>{let n,o,s=Pr(!1);return new ReadableStream({start(a){o=Nl(On(ML(e,i=>s.whenOpen(X(()=>{s.closeUnsafe();for(let c=0;c{i._tag===\"Failure\"?a.error(Pm(i.cause)):a.close()})},pull(){return new Promise(a=>{n=a,s.openUnsafe()})},cancel(){if(o)return tE(ia(mo(o)))}},r?.strategy)}),U7=l(e=>Ie(e[0]),(e,t)=>I0(e,Ir(),t)),D7=l(e=>Ie(e[0]),(e,t)=>j(hi(),r=>I0(e,r,t))),T0=l(2,(e,t)=>({[Symbol.asyncIterator](){let r=rE(t),n=Rn(t),o=kn(),s,a,i,c,u=p=>{if(c)return c;let d=i;return c=r(Tt(_r(d?mo(d):Q,Le(o,p)),{done:!0,value:void 0})),c},f=async p=>{try{await u(p)}catch(d){await r(nE(\"Suppressed error while closing Stream async iterator\",d))}};return{async next(){if(c)return c;if(a){let m=a.next();if(!m.done)return m;a=void 0}let p=n(s??I(ts(e.channel,o),m=>(s=m,m)));i=p;let d=await r(JD(p));if(i===p&&(i=void 0),Ve(d))return a=d.value[Symbol.iterator](),a.next();if(Bo(d.cause))return u(xt);if(c&&ob(d.cause))return c;throw await f(d),Pm(d.cause)},return(){return u(xt)},async throw(p){throw await f(lb(p)),p}}}})),L7=e=>j(hi(),t=>T0(e,t)),$7=e=>T0(e,Ir()),B7=l(e=>Ie(e[0]),(e,t,r)=>i0(e.channel,t,r)),j7=l(2,(e,t)=>v2(e.channel,t)),FL=l(2,(e,t)=>O2(e.channel,t)),z7=l(2,(e,t)=>A2(e.channel,t)),q7=l(2,(e,t)=>a0(e.channel,t));var w0=lr(\"effect/FileSystem\");var UL=\"~effect/Path\",A0=lr(\"effect/Path\");function DL(e,t){let r=\"\",n=0,o=-1,s=0,a;for(let i=0;i<=e.length;++i){if(i2){let c=r.lastIndexOf(\"/\");if(c!==r.length-1){c===-1?(r=\"\",n=0):(r=r.slice(0,c),n=r.length-1-r.lastIndexOf(\"/\")),o=i,s=0;continue}}else if(r.length===2||r.length===1){r=\"\",n=0,o=i,s=0;continue}}t&&(r.length>0?r+=\"/..\":r=\"..\",n=2)}else r.length>0?r+=\"/\"+e.slice(o+1,i):r=e.slice(o+1,i),n=i-o-1;o=i,s=0}else a===46&&s!==-1?++s:s=-1}return r}function H7(e,t){let r=t.dir||t.root,n=t.base||(t.name||\"\")+(t.ext||\"\");return r?r===t.root?r+n:r+e+n:n}function W7(e){if(e.protocol!==\"file:\")return P(new rf({module:\"Path\",method:\"fromFileUrl\",description:\"URL must be of scheme file\"}));if(e.hostname!==\"\")return P(new rf({module:\"Path\",method:\"fromFileUrl\",description:\"Invalid file URL host\"}));let t=e.pathname;for(let r=0;r=-1&&!r;o--){let s;if(o>=0)s=arguments[o];else{let a=globalThis.process;n===void 0&&\"process\"in globalThis&&typeof a==\"object\"&&a!==null&&typeof a.cwd==\"function\"&&(n=a.cwd()),s=n}s.length!==0&&(t=s+\"/\"+t,r=s.charCodeAt(0)===47)}return t=DL(t,!r),r?t.length>0?\"/\"+t:\"/\":t.length>0?t:\".\"},J7=47;function K7(e){let t=new URL(\"file://\"),r=LL(e);return e.charCodeAt(e.length-1)===J7&&r[r.length-1]!==\"/\"&&(r+=\"/\"),t.pathname=X7(r),g(t)}var G7=/%/g,V7=/\\\\/g,Z7=/\\n/g,Y7=/\\r/g,Q7=/\\t/g;function X7(e){return e.includes(\"%\")&&(e=e.replace(G7,\"%25\")),e.includes(\"\\\\\")&&(e=e.replace(V7,\"%5C\")),e.includes(`\n`)&&(e=e.replace(Z7,\"%0A\")),e.includes(\"\\r\")&&(e=e.replace(Y7,\"%0D\")),e.includes(\"\t\")&&(e=e.replace(Q7,\"%09\")),e}var C0=A0.of({[UL]:UL,resolve:LL,normalize(e){if(e.length===0)return\".\";let t=e.charCodeAt(0)===47,r=e.charCodeAt(e.length-1)===47;return e=DL(e,!t),e.length===0&&!t&&(e=\".\"),e.length>0&&r&&(e+=\"/\"),t?\"/\"+e:e},isAbsolute(e){return e.length>0&&e.charCodeAt(0)===47},join(){if(arguments.length===0)return\".\";let e;for(let t=0;t0&&(e===void 0?e=r:e+=\"/\"+r)}return e===void 0?\".\":C0.normalize(e)},relative(e,t){if(e===t||(e=C0.resolve(e),t=C0.resolve(t),e===t))return\"\";let r=1;for(;rc){if(t.charCodeAt(s+f)===47)return t.slice(s+f+1);if(f===0)return t.slice(s+f)}else o>c&&(e.charCodeAt(r+f)===47?u=f:f===0&&(u=0));break}let d=e.charCodeAt(r+f),m=t.charCodeAt(s+f);if(d!==m)break;d===47&&(u=f)}let p=\"\";for(f=r+u+1;f<=n;++f)(f===n||e.charCodeAt(f)===47)&&(p.length===0?p+=\"..\":p+=\"/..\");return p.length>0?p+t.slice(s+u):(s+=u,t.charCodeAt(s)===47&&++s,t.slice(s))},dirname(e){if(e.length===0)return\".\";let t=e.charCodeAt(0),r=t===47,n=-1,o=!0;for(let s=e.length-1;s>=1;--s)if(t=e.charCodeAt(s),t===47){if(!o){n=s;break}}else o=!1;return n===-1?r?\"/\":\".\":r&&n===1?\"//\":e.slice(0,n)},basename(e,t){let r=0,n=-1,o=!0,s;if(t!==void 0&&t.length>0&&t.length<=e.length){if(t.length===e.length&&t===e)return\"\";let a=t.length-1,i=-1;for(s=e.length-1;s>=0;--s){let c=e.charCodeAt(s);if(c===47){if(!o){r=s+1;break}}else i===-1&&(o=!1,i=s+1),a>=0&&(c===t.charCodeAt(a)?--a===-1&&(n=s):(a=-1,n=i))}return r===n?n=i:n===-1&&(n=e.length),e.slice(r,n)}else{for(s=e.length-1;s>=0;--s)if(e.charCodeAt(s)===47){if(!o){r=s+1;break}}else n===-1&&(o=!1,n=s+1);return n===-1?\"\":e.slice(r,n)}},extname(e){let t=-1,r=0,n=-1,o=!0,s=0;for(let a=e.length-1;a>=0;--a){let i=e.charCodeAt(a);if(i===47){if(!o){r=a+1;break}continue}n===-1&&(o=!1,n=a+1),i===46?t===-1?t=a:s!==1&&(s=1):t!==-1&&(s=-1)}return t===-1||n===-1||s===0||s===1&&t===n-1&&t===r+1?\"\":e.slice(t,n)},format:function(t){if(t===null||typeof t!=\"object\")throw new TypeError('The \"pathObject\" argument must be of type Object. Received type '+typeof t);return H7(\"/\",t)},parse(e){let t={root:\"\",dir:\"\",base:\"\",ext:\"\",name:\"\"};if(e.length===0)return t;let r=e.charCodeAt(0),n=r===47,o;n?(t.root=\"/\",o=1):o=0;let s=-1,a=0,i=-1,c=!0,u=e.length-1,f=0;for(;u>=o;--u){if(r=e.charCodeAt(u),r===47){if(!c){a=u+1;break}continue}i===-1&&(c=!1,i=u+1),r===46?s===-1?s=u:f!==1&&(f=1):s!==-1&&(f=-1)}return s===-1||i===-1||f===0||f===1&&s===i-1&&s===a+1?i!==-1&&(a===0&&n?t.base=t.name=e.slice(1,i):t.base=t.name=e.slice(a,i)):(a===0&&n?(t.name=e.slice(1,s),t.base=e.slice(1,i)):(t.name=e.slice(a,s),t.base=e.slice(a,i)),t.ext=e.slice(s,i)),a>0?t.dir=e.slice(0,a-1):n&&(t.dir=\"/\"),t},sep:\"/\",fromFileUrl:W7,toFileUrl:K7,toNamespacedPath:R});function Yp(e){return{_tag:\"Value\",value:e}}function wg(e,t){return{_tag:\"Record\",keys:e,value:t}}function O0(e,t){return{_tag:\"Array\",length:e,value:t}}var Tg=class extends Fr(\"SourceError\"){},ac=we(\"effect/ConfigProvider\",{defaultValue:()=>WL()}),eX={...rr,toJSON(){return{_id:\"ConfigProvider\"}}},tX=e=>e;function BL(e,t){let r=Object.create(eX);return r.load=e,r.mapInput=t,r}function jL(e,t){return BL(r=>e(t(r)),r=>jL(e,n=>r(t(n))))}function zL(e,t){return BL(r=>I(e.load(r),n=>n!==void 0?g(n):t.load(r)),r=>zL(e.mapInput(r),t.mapInput(r)))}function Cg(e){return jL(e,tX)}var v0=l(2,(e,t)=>zL(e,t)),_0=l(2,(e,t)=>e.mapInput(t)),rX=_0(e=>e.map(t=>typeof t==\"number\"?t:A_(t))),nX=l(2,(e,t)=>{let r=typeof t==\"string\"?[t]:t;return _0(e,n=>[...r,...n])}),oX=e=>jo(e)?yb(ac)(e):n_(ac)(e),sX=(e,t)=>yb(ac)(dr(function*(){let r=yield*ac,n=jo(e)?yield*e:e;return t?.asPrimary?v0(n,r):v0(r,n)}));function aX(e,t){let r=t?.preserveEmptyStrings===!0;return Cg(n=>g(iX(e,n,r)))}function iX(e,t,r){let n=e;for(let o of t){if(n==null)return;if(Array.isArray(n)){if(typeof o!=\"number\"||!Number.isInteger(o)||o<0||o>=n.length)return;n=n[o];continue}if(fr(n)){if(typeof o!=\"string\"||!Object.hasOwn(n,o))return;n=n[o];continue}return}return cX(n,r)}function cX(e,t){if(e!=null)return typeof e==\"string\"?qL(e,t):typeof e==\"number\"||typeof e==\"boolean\"||typeof e==\"bigint\"?Yp(String(e)):Array.isArray(e)?O0(e.length):fr(e)?wg(new Set(Object.keys(e))):Yp(F(e))}function qL(e,t){let r=HL(e,t);return r===void 0?void 0:Yp(r)}function HL(e,t){return e===\"\"&&!t?void 0:e}function R0(e,t){let r=t?.preserveEmptyStrings===!0,n=uX(e);return Cg(o=>g(fX(n,e,o,r)))}function WL(e){let t=e?.env??{...globalThis.process?.env,...import.meta?.env};return R0(t,{preserveEmptyStrings:e?.preserveEmptyStrings})}function uX(e){let t={};for(let[r,n]of Object.entries(e)){if(n===void 0)continue;let o=r.split(\"_\"),s=t;for(let a of o){let i=s.children??=Object.create(null);s=i[a]??={}}}return t}function fX(e,t,r,n){let o=r.map(String).join(\"_\"),s=HL(Object.hasOwn(t,o)?t[o]:void 0,n),a=lX(e,r),i=a?.children?Object.keys(a.children):[];if(i.length===0)return s===void 0?void 0:Yp(s);if(i.every(Cc)){let u=Math.max(...i.map(f=>parseInt(f,10)))+1;return O0(u,s)}return wg(new Set(i),s)}function lX(e,t){if(t.length===0)return e;let r=e;for(let n of t)if(r=r?.children?.[String(n)],!r)return;return r}function JL(e,t){let r=dX(e);return t?.expandVariables&&(r=mX(r)),R0(r,{preserveEmptyStrings:t?.preserveEmptyStrings})}var pX=/(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?\\s*(?:#.*)?(?:$|$)/mg;function dX(e){let t=Object.create(null);e=e.replace(/\\r\\n?/gm,`\n`);let r;for(;(r=pX.exec(e))!=null;){let n=r[1],o=r[2]||\"\";o=o.trim();let s=o[0];o=o.replace(/^(['\"`])([\\s\\S]*)\\1$/gm,\"$2\"),s==='\"'&&(o=o.replace(/\\\\n/g,`\n`),o=o.replace(/\\\\r/g,\"\\r\")),t[n]=o}return t}function mX(e){let t=Object.create(null);for(let r of Object.keys(e))t[r]=KL(e[r],e).replace(/\\\\\\$/g,\"$\");return t}function KL(e,t){let r=hX(e,/(?!(?<=\\\\))\\$/g);if(r===-1)return e;let n=e.slice(r),o=/((?!(?<=\\\\))\\${?([\\w]+)(?::-([^}\\\\]*))?}?)/,s=n.match(o);if(s!==null){let[a,i,c,u]=s,f=Object.hasOwn(t,c)&&t[c]!==\"\"?t[c]:u??\"\";return KL(e.replace(i,()=>f),t)}return e}function hX(e,t){let r=Array.from(e.matchAll(t));return r.length>0?r.slice(-1)[0].index:-1}var xX=Se(function*(e){let r=yield*(yield*w0).readFileString(e?.path??\".env\");return JL(r,e)}),gX=Se(function*(e){let t=yield*A0,r=yield*w0,n=e?.rootPath??\"/\",o=e?.preserveEmptyStrings===!0;return Cg(s=>{let a=t.join(n,...s.map(String)),i=r.readFileString(a).pipe(j(u=>qL(u.trim(),o))),c=r.readDirectory(a).pipe(j(u=>wg(new Set(u.map(f=>t.basename(f))))));return i.pipe(ca(u=>c.pipe(ca(f=>k0(u)&&k0(f)?g(void 0):P(k0(u)?f:u)))),th(u=>new Tg({message:`Failed to read file at ${t.join(n,...s.map(String))}`,cause:u})))})}),k0=e=>e.reason._tag===\"NotFound\";var GL=[\"All\",\"Fatal\",\"Error\",\"Warn\",\"Info\",\"Debug\",\"Trace\",\"None\"];var M0=\"~effect/Config\",YL=e=>k(e,M0),af=class{_tag=\"ConfigError\";name=\"ConfigError\";cause;constructor(t){this.cause=t}get message(){return this.cause.toString()}toString(){return`ConfigError(${this.message})`}},yX={...yd({label:\"Config\",evaluate(e){return this.parse(e.getRef(ac))}}),[M0]:M0,toJSON(){return{_id:\"Config\"}}};function os(e){let t=Object.create(yX);return t.evaluator=e,t.parse=r=>st(e(r,[]),Pl),t}var So=(e,t,r)=>e.evaluator(t,r),QL=l(2,(e,t)=>os((r,n)=>j(So(e,r,n),Tr(t)))),SX=l(2,(e,t)=>os((r,n)=>I(So(e,r,n),wr({onSuccess:o=>So(t(o),r,n),onFailure:o=>g(W(o))})))),bX=l(2,(e,t)=>os((r,n)=>I(So(e,r,n),o=>mt(o)?Dr(t(o.success),B):g(W(o.failure))))),EX=l(2,(e,t)=>os((r,n)=>zo(So(e,r,n),{onFailure:o=>{let s=ib(o);return be(s)&&o.reasons.every(na)?So(t(s.value),r,n):ze(o)},onSuccess:o=>ce(o)?So(t(o.failure),r,n):g(o)})));function XL(e){let t=globalThis.Array.isArray(e)?e:Symbol.iterator in e?[...e]:e;return os((r,n)=>globalThis.Array.isArray(t)?Dr(Ym(t.map(o=>So(o,r,n))),hy):Dr(Ym(Ya(t,o=>So(o,r,n))),hy))}var e$=l(2,(e,t)=>os((r,n)=>Dr(So(e,r,n),o=>ce(o)?B(t):o))),IX=e=>e.pipe(QL(O),e$(v())),t$=e=>YL(e)?e:XL(Ya(e,t=>t$(t))),TX=e=>At(e,\"SourceError\"),wX=()=>\"\",r$=e=>t=>rt(t,r=>ze(oa(r.reasons.map(e)))),n$=(e,t)=>e.load(t).pipe(r$(r=>na(r)?zc(r.error).annotate(Mm(r)):r),Dr(r=>({provider:e,path:t,node:r,toString:wX}))),ZL=(e,t)=>n$(e.provider,[...e.path,t]),P0=(e,t)=>ga(eo,e,new ve(gt(r=>t(r)),jr())),o$=e=>{switch(e._tag){case\"Union\":return e.types.every(o$);case\"Objects\":case\"Arrays\":case\"Suspend\":return!1;default:return!0}},F0=(e,t)=>{switch(e._tag){case\"Objects\":return t?._tag===\"Record\";case\"Arrays\":return t?._tag===\"Array\";case\"Union\":return e.types.some(r=>F0(r,t));case\"Suspend\":return F0(e.thunk(),t);default:return t?.value!==void 0}},CX=Vr(e=>{let t=new WeakSet,r=oF(n=>{switch(t.add(n),n._tag){case\"Objects\":{let o=n.indexSignatures.map(a=>Ui(a.parameter)),s=Se(function*(a){if(a.node?._tag!==\"Record\")return;let i=a.node,c=new Set;for(let f of n.propertySignatures)typeof f.name==\"string\"&&c.add(f.name);if(o.length>0)for(let f of i.keys)o.some(p=>p(f))&&c.add(f);let u={};for(let f of c){let p=yield*ZL(a,f);p.node!==void 0&&M(u,f,p)}return u});return P0(n.recur(r,a=>a),s)}case\"Arrays\":{let o=Se(function*(s){if(s.node?._tag!==\"Array\")return;let a=[];for(let i=0;ig(o.node?.value))});return r(e)});function tr(e,t){let r=Np(e),n=hn(r.ast),o=nt(q(CX(r.ast))),s=typeof t==\"string\"?[t]:t??[];return os((a,i)=>{let c=[...i,...s];return n$(a,c).pipe(st(u=>o(u).pipe(Dr(B),ru(f=>{let p=new af(new Os(c.length>0?new ut(c,f):f));return F0(n,u.node)?P(p):g(W(p))}))),r$(u=>rb(u)&&TX(u.defect)?jc(new af(u.defect)).annotate(Mm(u)):u))})}var AX=lo.check(Hu({minimum:1,maximum:65535})),kX=fo(GL),s$=e=>br(e)||globalThis.Array.isArray(e);function vX(e){return os(()=>P(new af(e)))}function OX(e){return os(()=>g(B(e)))}function _X(e){return tr(K,e)}function RX(e){return tr(fw,e)}function PX(e){return tr(Wi,e)}function MX(e){return tr(qu,e)}function FX(e){return tr(lo,e)}function NX(e,t){return tr(Ct(e),t)}function UX(e,t){return tr(fo(e),t)}function DX(e,t,r){let n=s$(t),o=n?r:t,s=$e(e),a=o?.separator??\",\",i=K.pipe(G(Np(s),{decode:zP(o),encode:Ch(jr({strict:!1}),oe(c=>c.join(a)))}));return tr(it([i,s]),n?t:void 0)}function LX(e,t,r,n){let o=s$(r),s=_s(e,t),a=oM(o?n:r),i=K.pipe(G(Np(s),{decode:a.decode,encode:Ch(jr({strict:!1}),a.encode)}));return tr(it([s,i]),o?r:void 0)}function $X(e){return tr(nw,e)}function BX(e){return tr(hw,e)}function jX(e){return tr(Ju,e)}function zX(e){return tr(AX,e)}function qX(e){return tr(kX,e)}function HX(e){return tr(Fx(K),e)}function WX(e){return tr(Ox,e)}function JX(e){return tr(Wu,e)}var KX=l(2,(e,t)=>os((r,n)=>So(e,r,[...n,t])));var ic={};Eo(ic,{Console:()=>a$,assert:()=>GX,clear:()=>VX,consoleWith:()=>jt,count:()=>ZX,countReset:()=>YX,debug:()=>QX,dir:()=>XX,dirxml:()=>eee,error:()=>tee,group:()=>ree,info:()=>nee,log:()=>oee,table:()=>see,time:()=>aee,timeLog:()=>iee,trace:()=>cee,warn:()=>uee,withGroup:()=>fee,withTime:()=>lee});var a$=VS,jt=e=>re(t=>e(t.getRef(a$))),GX=(e,...t)=>jt(r=>z(()=>{r.assert(e,...t)})),VX=jt(e=>z(()=>{e.clear()})),ZX=e=>jt(t=>z(()=>{t.count(e)})),YX=e=>jt(t=>z(()=>{t.countReset(e)})),QX=(...e)=>jt(t=>z(()=>{t.debug(...e)})),XX=(e,t)=>jt(r=>z(()=>{r.dir(e,t)})),eee=(...e)=>jt(t=>z(()=>{t.dirxml(...e)})),tee=(...e)=>jt(t=>z(()=>{t.error(...e)})),ree=e=>jt(t=>ai(z(()=>{e?.collapsed?t.groupCollapsed(e.label):t.group(e?.label)}),()=>z(()=>{t.groupEnd()}))),nee=(...e)=>jt(t=>z(()=>{t.info(...e)})),oee=(...e)=>jt(t=>z(()=>{t.log(...e)})),see=(e,t)=>jt(r=>z(()=>{r.table(e,t)})),aee=e=>jt(t=>ai(z(()=>{t.time(e)}),()=>z(()=>{t.timeEnd(e)}))),iee=(e,...t)=>jt(r=>z(()=>{r.timeLog(e,...t)})),cee=(...e)=>jt(t=>z(()=>{t.trace(...e)})),uee=(...e)=>jt(t=>z(()=>{t.warn(...e)})),fee=l(e=>ie(e[0]),(e,t)=>jt(r=>ul(z(()=>{t?.collapsed?r.groupCollapsed(t.label):r.group(t?.label)}),()=>e,()=>z(()=>{r.groupEnd()})))),lee=l(e=>ie(e[0]),(e,t)=>jt(r=>ul(z(()=>{r.time(t)}),()=>e,()=>z(()=>{r.timeEnd(t)}))));var Kr={OK:200,Unauthorized:401,NotFound:404,InternalServerError:500,ServiceUnavailable:503},N0={[Kr.OK]:\"OK\",[Kr.Unauthorized]:\"Unauthorized\",[Kr.NotFound]:\"Not Found\",[Kr.InternalServerError]:\"Internal Server Error\",[Kr.ServiceUnavailable]:\"Service Unavailable\"},pee=e=>{let t=e.startsWith(\"/\"),r=[];for(let o of e.split(\"/\"))if(!(o===\"\"||o===\".\")){if(o===\"..\"){r.length>0&&r.at(-1)!==\"..\"?r.pop():t||r.push(\"..\");continue}r.push(o)}let n=r.join(\"/\");return t?`/${n}`:n||\".\"},ka=(...e)=>pee(e.filter(Boolean).join(\"/\")),cf=e=>{if(e.length===0)return\".\";let t=e.length;for(;t>1&&e[t-1]===\"/\";)t-=1;let r=e.slice(0,t),n=r.lastIndexOf(\"/\");return n<0?\".\":n===0?\"/\":r.slice(0,n)},i$=e=>{if(!e.startsWith(\"/\"))throw new TypeError(\"Path must be absolute\");let t=new URL(\"file:///\");return t.pathname=e.replace(/%/g,\"%25\").replace(/\\\\/g,\"%5C\"),t};var dee=/^[A-Za-z0-9_-]+$/u,c$=/[*?[{]/u,Ag=(e,t)=>t===e||t.startsWith(`${e.replace(/\\/+$/u,\"\")}/`),uf=class extends Cn.TaggedError(\"FunctionFileSystemError\"){},Dn=(e,t)=>e.lstat(t).pipe(N.catch(()=>N.undefined)),cc=(e,t,r)=>N.all([e.realPath(t),e.realPath(r)],{concurrency:2}).pipe(N.map(([n,o])=>Ag(n,o)),N.orElseSucceed(()=>!1)),u$=(e,t,r)=>N.gen(function*(){let n=yield*Dn(e,r);if(n===void 0)return!0;if(n.isSymbolicLink||!(yield*cc(e,t,r)))return!1;if(!n.isDirectory)return!0;let o=yield*e.readDirectory(r).pipe(N.catch(()=>N.undefined));if(o===void 0)return!1;for(let s of o)if(!(yield*u$(e,t,ka(r,s))))return!1;return!0}),Qp=(e,t)=>t.length===0?\"\":t.startsWith(\"/\")?t:ka(e,t),U0=[\"deno.json\",\"deno.jsonc\"],f$=e=>U0.includes(e.slice(e.lastIndexOf(\"/\")+1)),mee=(e,t,r)=>N.gen(function*(){let n=cf(ka(r));for(;Ag(t,n);){for(let s of U0){let a=ka(n,s),i=yield*Dn(e,a);if(i!==void 0)return i.isFile&&!i.isSymbolicLink?a:void 0}let o=cf(n);if(o===n)break;n=o}}),hee=(e,t,r)=>N.all([e.realPath(t),e.realPath(r)],{concurrency:2}).pipe(N.map(([n,o])=>n===o),N.orElseSucceed(()=>!1)),l$=N.fn(\"Functions.resolveFunctionConfig\")(function*(e){let{root:t,slug:r,overrides:n,fs:o}=e;if(!t.startsWith(\"/\")||!dee.test(r)||r===\"_shared\"||(yield*Dn(o,t))===void 0)return;let a=yield*o.realPath(t).pipe(N.orElseSucceed(()=>\"\"));if(!a.startsWith(\"/\"))return;let i=yield*Dn(o,a);if(i===void 0||!i.isDirectory)return;let c=e.filesRoot===void 0?a:yield*o.realPath(e.filesRoot).pipe(N.orElseSucceed(()=>\"\"));if(!c.startsWith(\"/\")||!Ag(c,a))return;let u=n.$default,f=n[r],p={...u,...f};if(p.enabled===!1)return;let d=ka(a,r),m=p?.entrypointPath&&p.entrypointPath.length>0?p.entrypointPath:p.entrypoint&&p.entrypoint.length>0?p.entrypoint:\"index.ts\";if(!m.startsWith(\"/\")){let b=yield*Dn(o,d);if(b===void 0||!b.isDirectory||!(yield*cc(o,a,d)))return}let x=Qp(d,m);if(!(yield*cc(o,c,x)))return;let y=yield*Dn(o,x);if(y===void 0||!y.isFile||y.isSymbolicLink)return;let S=f?.importMapPath??f?.import_map,w=u?.importMapRoot??u?.import_map_root,E=S!==void 0?Qp(d,S):w!==void 0?Qp(a,w):Qp(d,\"\");if(E.length>0){if(!(yield*cc(o,c,E)))return;let b=yield*Dn(o,E);if(b===void 0||!b.isFile||b.isSymbolicLink)return}else for(let b of U0){let T=ka(d,b),C=yield*Dn(o,T);if(C!==void 0){if(!C.isFile||C.isSymbolicLink||!(yield*cc(o,c,T)))return;E=T;break}}let U=E.length===0?void 0:yield*mee(o,c,x),_=U!==void 0&&(yield*hee(o,U,E)),h=(p.staticFiles??p.static_files??[]).map(b=>Qp(d,b));for(let b of h){if(!Ag(c,b))return;let T=b.search(c$),C=T<0?b:b.slice(0,T),L=T<0?cf(b):C.slice(0,Math.max(0,C.lastIndexOf(\"/\")))||a;if(!(yield*u$(o,c,L)))return;if(!c$.test(b)){let ee=yield*Dn(o,b);if(ee!==void 0&&(!(yield*cc(o,c,b))||ee.isSymbolicLink))return}}return{entrypointPath:x,importMapPath:E,importMapDiscoveredByRuntime:_,staticFiles:h,verifyJWT:p.verifyJWT??p.verify_jwt??!0,env:p.env}}),xee=e=>ka(cf(e.entrypointPath),\"package.json\"),p$=e=>{let t=new Map,r=new Map;return(n,o)=>{let s=r.get(n);if(s!==void 0)return s;let a=cf(o.entrypointPath),i=t.get(a),c=i===void 0||i===n?a:e();return i===void 0&&t.set(a,n),r.set(n,c),c}},d$=N.fn(\"Functions.packageJsonContainedFor\")(function*(e){if(!e.root.startsWith(\"/\"))return!1;let t=yield*Dn(e.fs,e.root);if(t===void 0||!t.isDirectory&&!t.isSymbolicLink)return!1;let r=yield*e.fs.realPath(e.root).pipe(N.orElseSucceed(()=>\"\"));if(!r.startsWith(\"/\"))return!1;let n=yield*Dn(e.fs,r);if(n===void 0||!n.isDirectory)return!1;let o=xee(e.config),s=yield*Dn(e.fs,o);return s===void 0||!s.isFile||s.isSymbolicLink?!1:yield*cc(e.fs,r,o)});var D0=new TextEncoder,Xp=new TextDecoder,kg=new TextDecoder(\"utf-8\",{fatal:!0}),efe=2**32;function m$(...e){let t=e.reduce((o,{length:s})=>o+s,0),r=new Uint8Array(t),n=0;for(let o of e)r.set(o,n),n+=o.length;return r}function ff(e){let t=new Uint8Array(e.length);for(let r=0;r127)throw new TypeError(\"non-ASCII string encountered in encode()\");t[r]=n}return t}var vg=(e,t=\"algorithm.name\")=>new TypeError(`CryptoKey does not support this operation, its ${t} must be ${e}`);function gee(e,t){if(t&&!e.usages.includes(t))throw new TypeError(`CryptoKey does not support this operation, its usages must include ${t}.`)}function h$(e,t){let{modulusLength:r}=t.algorithm;if(typeof r!=\"number\"||r<2048)throw new TypeError(`${e} requires key modulusLength to be 2048 bits or larger`)}function x$(e,t,r){let n=e.algorithm;if(n.name!==t.name)throw vg(t.name);if(t.hash&&n.hash?.name!==t.hash)throw vg(t.hash,\"algorithm.hash\");if(t.namedCurve&&n.namedCurve!==t.namedCurve)throw vg(t.namedCurve,\"algorithm.namedCurve\");if(t.length!==void 0&&n.length!==t.length)throw vg(t.length,\"algorithm.length\");gee(e,r)}function yee(e,t,...r){if(r.length>2){let n=r.pop();e+=`one of type ${r.join(\", \")}, or ${n}.`}else r.length===2?e+=`one of type ${r[0]} or ${r[1]}.`:e+=`of type ${r[0]}.`;return t==null?e+=` Received ${t}`:typeof t==\"function\"&&t.name?e+=` Received function ${t.name}`:typeof t==\"object\"&&t!=null&&t.constructor?.name&&(e+=` Received an instance of ${t.constructor.name}`),e}var ed=(e,t,...r)=>yee(`Key for the ${e} algorithm must be `,t,...r);var yr=class extends Error{static code=\"ERR_JOSE_GENERIC\";code=\"ERR_JOSE_GENERIC\";constructor(t,r){super(t,r),this.name=this.constructor.name,Error.captureStackTrace?.(this,this.constructor)}},Us=class extends yr{static code=\"ERR_JWT_CLAIM_VALIDATION_FAILED\";code=\"ERR_JWT_CLAIM_VALIDATION_FAILED\";claim;reason;payload;constructor(t,r,n=\"unspecified\",o=\"unspecified\"){super(t,{cause:{claim:n,reason:o,payload:r}}),this.claim=n,this.reason=o,this.payload=r}},td=class extends yr{static code=\"ERR_JWT_EXPIRED\";code=\"ERR_JWT_EXPIRED\";claim;reason;payload;constructor(t,r,n=\"unspecified\",o=\"unspecified\"){super(t,{cause:{claim:n,reason:o,payload:r}}),this.claim=n,this.reason=o,this.payload=r}},Og=class extends yr{static code=\"ERR_JOSE_ALG_NOT_ALLOWED\";code=\"ERR_JOSE_ALG_NOT_ALLOWED\"},bo=class extends yr{static code=\"ERR_JOSE_NOT_SUPPORTED\";code=\"ERR_JOSE_NOT_SUPPORTED\"};var Gr=class extends yr{static code=\"ERR_JWS_INVALID\";code=\"ERR_JWS_INVALID\"},lf=class extends yr{static code=\"ERR_JWT_INVALID\";code=\"ERR_JWT_INVALID\"};var rd=class extends yr{static code=\"ERR_JWKS_INVALID\";code=\"ERR_JWKS_INVALID\"},pf=class extends yr{static code=\"ERR_JWKS_NO_MATCHING_KEY\";code=\"ERR_JWKS_NO_MATCHING_KEY\";constructor(t=\"no applicable key found in the JSON Web Key Set\",r){super(t,r)}},_g=class extends yr{[Symbol.asyncIterator]=async function*(){};static code=\"ERR_JWKS_MULTIPLE_MATCHING_KEYS\";code=\"ERR_JWKS_MULTIPLE_MATCHING_KEYS\";constructor(t=\"multiple matching keys found in the JSON Web Key Set\",r){super(t,r)}},Rg=class extends yr{static code=\"ERR_JWKS_TIMEOUT\";code=\"ERR_JWKS_TIMEOUT\";constructor(t=\"request timed out\",r){super(t,r)}},Pg=class extends yr{static code=\"ERR_JWS_SIGNATURE_VERIFICATION_FAILED\";code=\"ERR_JWS_SIGNATURE_VERIFICATION_FAILED\";constructor(t=\"signature verification failed\",r){super(t,r)}};var L0=e=>{if(e?.[Symbol.toStringTag]===\"CryptoKey\")return!0;try{return e instanceof CryptoKey}catch{return!1}},See=e=>e?.[Symbol.toStringTag]===\"KeyObject\",g$=e=>L0(e)||See(e);function y$(e){if(Uint8Array.fromBase64)return Uint8Array.fromBase64(e);let t=atob(e),r=new Uint8Array(t.length);for(let n=0;ntypeof o!=\"string\")||new Set(n).size!==n.length)throw new TypeError('\"key_ops\" (Key Operations) Parameter must be an array of unique strings');t.key_ops=n}return t}var z0=e=>e[Symbol.toStringTag],bee=(e,t,r)=>{let{alg:n}=e;if(t.use!==void 0){let o=r===\"sign\"||r===\"verify\"?\"sig\":\"enc\";if(t.use!==o)throw new TypeError(`Invalid key for this operation, its \"use\" must be \"${o}\" when present`)}if(t.alg!==void 0&&t.alg!==n)throw new TypeError(`Invalid key for this operation, its \"alg\" must be \"${n}\" when present`);if(Array.isArray(t.key_ops)){let o=r===\"encrypt\"||r===\"decrypt\"?e.ops?.[r===\"encrypt\"?0:1]:r;if(o&&!t.key_ops.includes(o))throw new TypeError(`Invalid key for this operation, its \"key_ops\" must include \"${o}\" when present`)}};function Eee(e,t,r){let{alg:n,secret:o}=e,s=r===\"decrypt\"||r===\"sign\";if(o&&t instanceof Uint8Array)return[I$,t];if(va(t)){let a=b$(t);if(typeof a.kty!=\"string\")throw new TypeError(o?ed(n,t,\"CryptoKey\",\"KeyObject\",\"JSON Web Key\",\"Uint8Array\"):ed(n,t,\"CryptoKey\",\"KeyObject\",\"JSON Web Key\"));if(!(o?a.kty===\"oct\"&&typeof a.k==\"string\":a.kty!==\"oct\"&&(s?a.kty===\"AKP\"&&typeof a.priv==\"string\"||typeof a.d==\"string\":a.d===void 0&&a.priv===void 0)))throw new TypeError(o?'JSON Web Key for symmetric algorithms must have JWK \"kty\" (Key Type) equal to \"oct\" and the JWK \"k\" (Key Value) present':`JSON Web Key for this operation must be a ${s?\"private\":\"public\"} JWK`);return bee(e,a,r),[C$,t,a]}if(!g$(t))throw new TypeError(o?ed(n,t,\"CryptoKey\",\"KeyObject\",\"JSON Web Key\",\"Uint8Array\"):ed(n,t,\"CryptoKey\",\"KeyObject\",\"JSON Web Key\"));if(o){if(t.type!==\"secret\")throw new TypeError(`${z0(t)} instances for symmetric algorithms must be of type \"secret\"`)}else{if(t.type===\"secret\")throw new TypeError(`${z0(t)} instances for asymmetric algorithms must not be of type \"secret\"`);let a=s?\"private\":\"public\";if((t.type===\"public\"||t.type===\"private\")&&t.type!==a){let i=r===\"sign\"?\"signing\":r===\"verify\"?\"verifying\":`${r.slice(0,-1)}tion`;throw new TypeError(`${z0(t)} instances for asymmetric algorithm ${i} must be of type \"${a}\"`)}}return L0(t)?[T$,t]:[w$,t]}var I$=0,T$=1,w$=2,C$=3,q0,Iee={__proto__:null,prime256v1:\"P-256\",secp384r1:\"P-384\",secp521r1:\"P-521\"};function Ug(e,t,r){q0||=new WeakMap;let n=q0.get(e);return r&&(n?n[t]=r:q0.set(e,{[t]:r})),r??n?.[t]}var E$=async(e,t,r)=>Ug(e,r.alg)??Ug(e,r.alg,await Ng(r,{...t,alg:r.alg})),Tee=(e,t)=>{let r=Ug(e,t.alg);if(r)return r;let n=e.type===\"public\",o=t.usages[n?0:1],{asymmetricKeyType:s}=e,a=Iee[e.asymmetricKeyDetails?.namedCurve],i=t.resolve?.({crv:a,asymmetricKeyType:s})??t.subtle;return Ug(e,t.alg,e.toCryptoKey(i,n,o))};async function A$(e,t,r){let n=Eee(e,t,r);switch(n[0]){case I$:case T$:return n[1];case C$:{let o=n[1],s=n[2];if(s.kty===\"oct\")return nd(s.k);if(!Object.isFrozen(o)){let{key_ops:a}=o;Array.isArray(a)&&Object.freeze(a),Object.freeze(o)}return E$(o,s,e)}case w$:{let o=n[1];return o.type===\"secret\"?o.export():\"toCryptoKey\"in o&&typeof o.toCryptoKey==\"function\"?Tee(o,e):E$(o,o.export({format:\"jwk\"}),e)}}}function k$(e){let t={__proto__:null};for(let r in e)t[r]={...e[r],alg:r};return t}var v$={__proto__:null,b64:!0};function O$(e,t){if(t!==void 0&&(!Array.isArray(t)||t.some(r=>typeof r!=\"string\")))throw new TypeError(`\"${e}\" option must be an array of strings`);if(t)return new Set(t)}function _$(e,t,r,n,o){if(o.crit!==void 0&&n?.crit===void 0)throw new e('\"crit\" (Critical) Header Parameter MUST be integrity protected');if(!n||n.crit===void 0)return[];if(!Array.isArray(n.crit)||n.crit.length===0||n.crit.some(a=>typeof a!=\"string\"||a.length===0))throw new e('\"crit\" (Critical) Header Parameter MUST be an array of non-empty strings when present');let s=r===void 0?t:{__proto__:null,...r,...t};for(let a of n.crit){if(!(a in s))throw new bo(`Extension Header Parameter \"${a}\" is not recognized`);if(!Object.hasOwn(o,a)||o[a]===void 0)throw new e(`Extension Header Parameter \"${a}\" is missing`);if(s[a]&&(!Object.hasOwn(n,a)||n[a]===void 0))throw new e(`Extension Header Parameter \"${a}\" MUST be integrity protected`)}return n.crit}function R$(e,t){if(t.includes(\"b64\")){let r=e.b64;if(typeof r!=\"boolean\")throw new Gr('The \"b64\" (base64url-encode payload) Header Parameter must be a boolean');return r}return!0}async function wee(e,t,r){return t instanceof Uint8Array?crypto.subtle.importKey(\"raw\",t,e.subtle,!1,[r]):(x$(t,e.subtle,r),e.minRsaBits&&h$(e.alg,t),t)}async function P$(e,t,r,n){let o=await wee(e,t,\"verify\");try{return await crypto.subtle.verify(e.signing,o,r,n)}catch{return!1}}var od=[[\"verify\"],[\"sign\"]];function H0(e){let t={name:\"HMAC\",hash:`SHA-${e}`};return{kty:[\"oct\"],secret:!0,subtle:t,signing:t,usages:od}}function df(e,t){let n={name:t?\"RSA-PSS\":\"RSASSA-PKCS1-v1_5\",hash:`SHA-${e}`};return{kty:[\"RSA\"],subtle:n,signing:t?{...n,saltLength:t}:n,usages:od,minRsaBits:2048}}function W0(e,t){return{kty:[\"EC\"],crv:e,subtle:{name:\"ECDSA\",namedCurve:e},signing:{name:\"ECDSA\",hash:`SHA-${t}`},usages:od}}function M$(){let e={name:\"Ed25519\"};return{kty:[\"OKP\"],crv:\"Ed25519\",subtle:e,signing:e,usages:od}}function J0(e){let r={name:`ML-DSA-${e}`};return{kty:[\"AKP\"],subtle:r,signing:r,usages:od}}var K0=k$({HS256:H0(256),HS384:H0(384),HS512:H0(512),RS256:df(256),RS384:df(384),RS512:df(512),PS256:df(256,32),PS384:df(384,48),PS512:df(512,64),ES256:W0(\"P-256\",256),ES384:W0(\"P-384\",384),ES512:W0(\"P-521\",512),EdDSA:M$(),Ed25519:M$(),\"ML-DSA-44\":J0(44),\"ML-DSA-65\":J0(65),\"ML-DSA-87\":J0(87)});function F$(e){let t=typeof e==\"string\"?K0[e]:void 0;if(!t)throw new bo(`alg ${e} is not supported either by JOSE or your javascript runtime`);return t}function N$(e){return[e&&O$(\"algorithms\",e.algorithms),e?.crit]}function Cee(e,t=e===void 0?{}:Fg(e,Gr,\"JWS Protected Header is invalid\")){return t}function Aee(e,t,r){let n=R$(e,_$(Gr,v$,r[1],e,t)),o=t.alg;if(typeof o!=\"string\"||!o)throw new Gr('JWS \"alg\" (Algorithm) Header Parameter missing or invalid');if(r[0]&&!r[0].has(o))throw new Og('\"alg\" (Algorithm) Header Parameter value not allowed');return[n,o]}function kee(e){try{return ff(e)}catch{throw new Gr(\"JWS Compact Serialization payload must use only ASCII characters\")}}async function vee(e,t,r,n,o,s,a){let i=!1;typeof r==\"function\"&&(r=await r(o,e),i=!0);let c=typeof a==\"string\",u=F$(s),f=m$(n!==void 0?ff(n):new Uint8Array,ff(\".\"),c?t[2]??=S$(a,\"payload\",Gr):a),p=B0(e.signature,\"signature\",Gr),d=await A$(u,r,\"verify\");if(!await P$(u,d,p,f))throw new Pg;return[c?B0(a,\"payload\",Gr):a,o,c,d,i]}async function U$(e,t,r){if(e instanceof Uint8Array&&(e=Xp.decode(e)),typeof e!=\"string\")throw new Gr(\"Compact JWS must be a string or Uint8Array\");let{0:n,1:o,2:s,length:a}=e.split(\".\");if(a!==3)throw new Gr(\"Invalid Compact JWS\");let i={payload:o,protected:n,signature:s},c=Cee(n),[u,f]=Aee(c,c,t),p=u?o:kee(o);return vee(i,t,r,n,c,f,p)}var Oee=e=>Math.floor(e.getTime()/1e3),_ee={s:1,m:60,h:3600,d:86400,w:604800,y:31557600},Ree=/^(\\+|\\-)? ?(\\d+|\\d+\\.\\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i,mf=\"check_failed\";function G0(){throw new TypeError(\"Invalid time period format\")}function D$(e){typeof e!=\"string\"&&G0();let t=Ree.exec(e);(!t||t[4]&&t[1])&&G0();let r=parseFloat(t[2]),n=Math.round(r*_ee[t[3][0].toLowerCase()]);return Number.isFinite(n)||G0(),t[1]===\"-\"||t[4]===\"ago\"?-n:n}function V0(e,t){if(!Number.isFinite(t))throw new TypeError(`Invalid ${e} input`);return t}var L$=e=>{let t=e.toLowerCase();return e.includes(\"/\")?t:`application/${t}`},Pee=(e,t)=>typeof e==\"string\"?t.includes(e):Array.isArray(e)?t.some(r=>e.includes(r)):!1;function Z0(e,t,r=!1){let n=e[t];if(!(n===void 0&&!r)){if(typeof n!=\"number\")throw new Us(`\"${t}\" claim must be a number`,e,t,\"invalid\");return n}}function Y0(e,t){throw new Us(`unexpected \"${t}\" claim value`,e,t,mf)}function $$(e,t,r={}){let n;try{n=JSON.parse(kg.decode(t))}catch{}if(!va(n))throw new lf(\"JWT Claims Set must be a top-level JSON object\");let{typ:o}=r;if(o!==void 0&&(typeof e.typ!=\"string\"||L$(e.typ)!==L$(o)))throw new Us('unexpected \"typ\" JWT header value',n,\"typ\",mf);let{requiredClaims:s=[],issuer:a,subject:i,audience:c,maxTokenAge:u}=r,f=[...s];u!==void 0&&f.push(\"iat\"),c!==void 0&&f.push(\"aud\"),i!==void 0&&f.push(\"sub\"),a!==void 0&&f.push(\"iss\");for(let E of new Set(f.reverse()))if(!Object.hasOwn(n,E))throw new Us(`missing required \"${E}\" claim`,n,E,\"missing\");a!==void 0&&!(Array.isArray(a)?a:[a]).includes(n.iss)&&Y0(n,\"iss\"),i!==void 0&&n.sub!==i&&Y0(n,\"sub\"),c!==void 0&&!Pee(n.aud,typeof c==\"string\"?[c]:c)&&Y0(n,\"aud\");let{clockTolerance:p}=r,d=0;if(typeof p==\"string\")d=D$(p);else if(p!==void 0){if(typeof p!=\"number\")throw new TypeError(\"Invalid clockTolerance option type\");d=p}V0(\"clockTolerance option\",d);let{currentDate:m}=r,x=V0(\"currentDate option\",Oee(m===void 0?new Date:m)),y=Z0(n,\"iat\",u!==void 0),S=Z0(n,\"nbf\");if(S!==void 0&&S>x+d)throw new Us('\"nbf\" claim timestamp check failed',n,\"nbf\",mf);let w=Z0(n,\"exp\");if(w!==void 0&&w<=x-d)throw new td('\"exp\" claim timestamp check failed',n,\"exp\",mf);if(u!==void 0){let E=x-y,U=V0(\"maxTokenAge option\",typeof u==\"number\"?u:D$(u));if(E-d>U)throw new td('\"iat\" claim timestamp check failed (too far in the past)',n,\"iat\",mf);if(E<-d)throw new Us('\"iat\" claim timestamp check failed (it should be in the past)',n,\"iat\",mf)}return n}async function Dg(e,t,r){let n=await U$(e,N$(r),t);if(!n[2])throw new lf(\"JWTs MUST NOT use unencoded payload\");let s={payload:$$(n[1],n[0],r),protectedHeader:n[1]};return typeof t==\"function\"?{...s,key:n[3]}:s}function Mee(e,t,r,n){let{kty:o,key_ops:s,ext:a,kid:i,alg:c,use:u,crv:f}=j0(e),p=Array.isArray(s)?[...s]:s;return(a===void 0||typeof a==\"boolean\")&&(p===void 0||Array.isArray(p)&&p.every((d,m)=>typeof d==\"string\"&&p.indexOf(d)===m)&&p.includes(\"verify\"))&&t.kty.includes(o)&&(n===void 0||typeof n==\"string\"&&n===i)&&(c===void 0?o!==\"AKP\":r===c)&&(u===void 0||u===\"sig\")&&(!t.crv||f===t.crv)}async function B$(e,t,r){let n=e.get(t)||e.set(t,{}).get(t),{alg:o}=r;if(n[o]===void 0){let s=await Ng(r,{...t,alg:o,ext:!0});if(s.type!==\"public\")throw new rd(\"JSON Web Key Set members must be public keys\");n[o]=s}return n[o]}function uc(e){let t;try{t=structuredClone(e)}catch{}if(!Mg(t))throw new rd(\"JSON Web Key Set malformed\");let r=new WeakMap;return Object.defineProperty(async(o,s)=>{let{alg:a,kid:i}={...o,...s?.header},c=typeof a==\"string\"?K0[a]:void 0;if(!c||c.secret)throw new bo('Unsupported \"alg\" value for a JSON Web Key Set');let u=t.keys.filter(d=>Mee(d,c,a,i)),{0:f,length:p}=u;if(!p)throw new pf;if(p!==1){let d=new _g;throw d[Symbol.asyncIterator]=async function*(){for(let m of u)try{yield await B$(r,m,c)}catch{}},d}return B$(r,f,c)},\"jwks\",{value:()=>structuredClone(t)})}function Fee(){return typeof WebSocketPair<\"u\"||typeof navigator<\"u\"&&navigator.userAgent===\"Cloudflare-Workers\"||typeof EdgeRuntime<\"u\"&&EdgeRuntime===\"vercel\"}var Q0;(typeof navigator>\"u\"||!navigator.userAgent?.startsWith?.(\"Mozilla/5.0 \"))&&(Q0=\"jose/v6.2.10\");var z$=Symbol();async function Nee(e,t,r,n=fetch){let o=await n(e,{method:\"GET\",signal:r,redirect:\"manual\",headers:t}).catch(s=>{throw s.name===\"TimeoutError\"?new Rg:s});if(o.status!==200)throw new yr(\"Expected 200 OK from the JSON Web Key Set HTTP response\");try{return await o.json()}catch{throw new yr(\"Failed to parse the JSON Web Key Set HTTP response as JSON\")}}var q$=Symbol();function sd(e,t){return Number.isFinite(e)&&Date.now(){if(d&&Fee()&&(d=void 0),!d){let E=++m,U=d=Nee(r,c,AbortSignal.timeout(s),u).then(_=>{let h=uc(_);if(E<=x)return;y=h;let b=Date.now();f&&(f.uat=b,f.jwks=_),p=b,x=E}).finally(()=>{d===U&&(d=void 0)})}await d};return Object.defineProperties(async(E,U)=>{(!y||!sd(p,i))&&await S();try{return await y(E,U)}catch(_){if(_ instanceof pf&&!sd(p,a))return await S(),y(E,U);throw _}},{coolingDown:{get:()=>sd(p,a),enumerable:!0},fresh:{get:()=>sd(p,i),enumerable:!0},reload:{value:S,enumerable:!0},reloading:{get:()=>!!d,enumerable:!0},jwks:{value:()=>y?.jwks(),enumerable:!0}})}function eC(e){let t;if(typeof e==\"string\"){let n=e.split(\".\");(n.length===3||n.length===5)&&([t]=n)}else if(typeof e==\"object\"&&e)if(\"protected\"in e)t=e.protected;else throw new TypeError(\"Token does not contain a Protected Header\");let r=\"Invalid Token or Protected Header formatting\";if(typeof t!=\"string\"||!t)throw new TypeError(r);return Fg(t,TypeError,r)}var Dee=new Set([\"HOME\",\"HOSTNAME\",\"PATH\",\"PWD\"]),Ln=N.runSync(N.gen(function*(){let e=t=>t.pipe(Mr.option);return{hostPort:yield*e(Mr.String(\"SUPABASE_INTERNAL_HOST_PORT\")),functionsRoot:yield*e(Mr.String(\"SUPABASE_INTERNAL_FUNCTIONS_ROOT\")),filesRoot:yield*e(Mr.String(\"SUPABASE_INTERNAL_FUNCTIONS_FILES_ROOT\")),jwtSecret:yield*e(Mr.String(\"SUPABASE_INTERNAL_JWT_SECRET\")),supabaseUrl:yield*e(Mr.String(\"SUPABASE_URL\")),wallclock:yield*e(Mr.String(\"SUPABASE_INTERNAL_WALLCLOCK_LIMIT_SEC\")),publishableKey:yield*e(Mr.String(\"SUPABASE_INTERNAL_PUBLISHABLE_KEY\")),secretKey:yield*e(Mr.String(\"SUPABASE_INTERNAL_SECRET_KEY\")),functionsConfig:yield*e(Mr.String(\"SUPABASE_INTERNAL_FUNCTIONS_CONFIG\")),debug:yield*e(Mr.String(\"SUPABASE_INTERNAL_DEBUG\")),jwks:yield*e(Mr.String(\"SUPABASE_JWKS\"))}}).pipe(N.provideService(sf.ConfigProvider,sf.fromEnvRecord(Deno.env.toObject(),{preserveEmptyStrings:!0})))),xf=(e,t=\"\")=>xe.getOrElse(e,()=>t),H$=xf(Ln.hostPort,\"8081\"),Z$=xf(Ln.functionsRoot),Lee=xf(Ln.jwtSecret),$ee=xf(Ln.supabaseUrl,\"http://127.0.0.1:54321\"),Bee=new URL(\"/auth/v1/.well-known/jwks.json\",$ee),W$=Number.parseInt(xf(Ln.wallclock,\"400\"),10),J$=xe.getOrUndefined(Ln.publishableKey),K$=xe.getOrUndefined(Ln.secretKey),Ds={BootError:Kr.ServiceUnavailable,InvalidWorkerResponse:Kr.InternalServerError,WorkerLimit:546},jee={[Ds.BootError]:\"BOOT_ERROR\",[Ds.InvalidWorkerResponse]:\"WORKER_ERROR\",[Ds.WorkerLimit]:\"WORKER_LIMIT\"},zee={[Ds.BootError]:\"Worker failed to boot (please check logs)\",[Ds.InvalidWorkerResponse]:\"Function exited due to an error (please check logs)\",[Ds.WorkerLimit]:\"Worker failed to respond due to a resource limit (please check logs)\"},qee=new Map([[Deno.errors.InvalidWorkerCreation,Ds.BootError],[Deno.errors.InvalidWorkerResponse,Ds.InvalidWorkerResponse],[Deno.errors.WorkerRequestCancelled,Ds.WorkerLimit]]),Hee=e=>{let t=Deno.errors.WorkerAlreadyRetired;return t!==void 0&&e instanceof t},Wee=(s=>(s.MissingAuthHeader=\"UNAUTHORIZED_NO_AUTH_HEADER\",s.InvalidLegacyJWT=\"UNAUTHORIZED_JWT\",s.InvalidAsymmetricJWT=\"UNAUTHORIZED_ASYMMETRIC_JWT\",s.InvalidTokenFormat=\"UNAUTHORIZED_INVALID_JWT_FORMAT\",s.UnsupportedTokenAlgorithm=\"UNAUTHORIZED_UNSUPPORTED_TOKEN_ALGORITHM\",s))(Wee||{}),Jee=me.Struct({enabled:me.optionalKey(me.Boolean),verifyJWT:me.optionalKey(me.Boolean),verify_jwt:me.optionalKey(me.Boolean),entrypointPath:me.optionalKey(me.String),entrypoint:me.optionalKey(me.String),importMapPath:me.optionalKey(me.String),import_map:me.optionalKey(me.String),importMapRoot:me.optionalKey(me.String),import_map_root:me.optionalKey(me.String),staticFiles:me.optionalKey(me.Array(me.String)),static_files:me.optionalKey(me.Array(me.String)),env:me.optionalKey(me.Record(me.String,me.String))}),Kee=me.Record(me.String,Jee),Gee=me.declare(e=>typeof e==\"object\"&&e!==null&&\"keys\"in e&&Array.isArray(e.keys)&&e.keys.every(t=>typeof t==\"object\"&&t!==null)),Vee=()=>xe.match(Ln.functionsConfig,{onNone:()=>({}),onSome:e=>N.runSync(me.decodeEffect(me.fromJsonString(Kee))(e).pipe(N.orElseSucceed(()=>({}))))}),tC=Vee();if(xe.getOrUndefined(Ln.debug)===\"true\"){let e=Object.fromEntries(Object.entries(tC).map(([t,r])=>[t,Object.fromEntries(Object.entries(r).filter(([n])=>n!==\"env\"))]));N.runSync(ic.log(\"Functions config:\",JSON.stringify(e,null,2)))}var fc=(e,t,r={})=>{let n={...r},o=null;return e!=null&&(typeof e==\"object\"?(n[\"Content-Type\"]=\"application/json\",o=JSON.stringify(e)):(n[\"Content-Type\"]=\"text/plain\",o=typeof e==\"string\"?e:JSON.stringify(e)??null)),new Response(o,{status:t,headers:n})},G$=({code:e,message:t=\"Invalid JWT\"})=>fc({code:e,message:t,msg:t},Kr.Unauthorized,{\"sb-error-code\":e,\"Access-Control-Expose-Headers\":\"sb-error-code\"}),Zee=e=>{for(let[t,r]of qee.entries())if(t!==void 0&&e instanceof t)return fc({code:jee[r],message:zee[r]},r);return fc({code:N0[Kr.InternalServerError],message:\"Request failed due to an internal server error\"},Kr.InternalServerError)};function Yee(e){let t=e.split(\" \");return t.length===2&&t[0]===\"Bearer\"?t[1]:null}var Qee=e=>{let t=e.headers.get(\"authorization\"),r=e.headers.get(\"sb-api-key\")?.replace(\"Bearer\",\"\").trim();if(!t&&!r)return{code:\"UNAUTHORIZED_NO_AUTH_HEADER\",message:\"Missing authorization header\"};let n=Yee(t??\"\"),o=!n||n.startsWith(\"sb_\")?r:n;return o||{code:\"UNAUTHORIZED_INVALID_JWT_FORMAT\",message:\"Invalid JWT format\"}},hf=class extends Cn.TaggedError(\"BootstrapOperationError\"){},Xee=N.runSync(xe.match(Ln.jwks,{onNone:()=>N.succeed(xe.some(uc({keys:[]}))),onSome:e=>N.gen(function*(){let t=yield*me.decodeEffect(me.fromJsonString(Gee))(e);return yield*N.try({try:()=>uc(t),catch:r=>new hf({cause:r})})}).pipe(N.option)})),ete=xe.getOrElse(Xee,()=>X0(Bee)),Oa=e=>N.tryPromise({try:e,catch:t=>new hf({cause:t})}),tte=e=>N.gen(function*(){return yield*Oa(()=>Dg(e,ete)),xe.none()}).pipe(N.orElseSucceed(()=>xe.some({code:\"UNAUTHORIZED_ASYMMETRIC_JWT\"})));function rte(e,t){return N.gen(function*(){let r=yield*N.try({try:()=>eC(t).alg,catch:n=>new hf({cause:n})});return r?r===\"HS256\"?yield*Oa(()=>Dg(t,new TextEncoder().encode(e))).pipe(N.as(xe.none()),N.orElseSucceed(()=>xe.some({code:\"UNAUTHORIZED_JWT\"}))):r===\"ES256\"||r===\"RS256\"?yield*tte(t):xe.some({code:\"UNAUTHORIZED_UNSUPPORTED_TOKEN_ALGORITHM\",message:`Unsupported JWT algorithm ${r}`}):xe.some({code:\"UNAUTHORIZED_INVALID_JWT_FORMAT\",message:\"Invalid JWT format\"})}).pipe(N.orElseSucceed(()=>xe.some({code:\"UNAUTHORIZED_INVALID_JWT_FORMAT\",message:\"Invalid JWT format\"})))}var Y$={lstat:e=>Oa(()=>Deno.lstat(e)).pipe(N.mapError(t=>new uf({cause:t})),N.map(t=>({isDirectory:t.isDirectory,isFile:t.isFile,isSymbolicLink:t.isSymlink}))),realPath:e=>Oa(()=>Deno.realPath(e)).pipe(N.mapError(t=>new uf({cause:t}))),readDirectory:e=>ns.suspend(()=>ns.fromAsyncIterable(Deno.readDir(e),t=>new hf({cause:t})).pipe(ns.map(t=>t.name))).pipe(ns.runCollect,N.mapError(t=>new uf({cause:t})))},nte=e=>l$({root:Z$,filesRoot:xe.getOrUndefined(Ln.filesRoot),slug:e,overrides:tC,fs:Y$}),V$=new Set,ote=(e,t)=>t.importMapDiscoveredByRuntime||!f$(t.importMapPath)||V$.has(e)?N.void:N.sync(()=>V$.add(e)).pipe(N.andThen(ic.warn(`[functions] ${e}: ${t.importMapPath} is not the nearest Deno config of ${t.entrypointPath}, so Edge Runtime loads it as a plain import map without comments or jsr:/npm: subpath imports. Move it next to the entrypoint or into a parent directory without a closer deno.json(c).`))),ste=p$(()=>Deno.makeTempDirSync({prefix:\"supabase-worker-\"})),ate=e=>e.importMapPath?N.succeed(!1):d$({root:xf(Ln.filesRoot,Z$),config:e,fs:Y$}),Q$=e=>ns.fromEffectRepeat(Oa(()=>e.read()).pipe(N.flatMap(t=>t.done?Ft.done():N.succeed(t.value)))),ite=e=>e===void 0?N.void:ns.runDrain(Q$(e)).pipe(N.ignore);function cte(e,t){let r=new URL(e.url),n=e.headers.get(\"x-forwarded-host\");n&&(r.hostname=n);let o=new Request(r.href,{method:e.method,headers:e.headers,body:t===void 0?null:ns.toReadableStream(Q$(t)),signal:e.signal,duplex:\"half\"});return o.headers.delete(\"sb-api-key\"),EdgeRuntime.applySupabaseTag(e,o),o}Deno.serve({handler:e=>N.runPromiseExit(N.gen(function*(){let t=yield*N.acquireRelease(N.sync(()=>e.body?.getReader()),u=>N.interruptible(ite(u))),{pathname:r}=new URL(e.url);if(r===\"/_internal/health\")return fc({message:\"ok\"},Kr.OK);if(r===\"/_internal/metric\")return Response.json(yield*Oa(()=>EdgeRuntime.getRuntimeMetrics()));let n=r.split(\"/\")[1];if(!n)return fc(\"Function not found\",Kr.NotFound);let o=yield*nte(n);if(!o)return fc(\"Function not found\",Kr.NotFound);if(yield*ote(n,o),e.method!==\"OPTIONS\"&&o.verifyJWT){let u=Qee(e);if(typeof u!=\"string\")return G$(u);let f=yield*rte(Lee,u);if(xe.isSome(f))return G$(f.value)}let s={...Deno.env.toObject(),...Object.fromEntries(Object.entries(o.env??{}).filter(([u])=>!u.startsWith(\"SUPABASE_\"))),SUPABASE_FUNCTION_SLUG:n};J$&&(s.SUPABASE_PUBLISHABLE_KEYS=yield*me.encodeEffect(me.fromJsonString(me.Unknown))({default:J$})),K$&&(s.SUPABASE_SECRET_KEYS=yield*me.encodeEffect(me.fromJsonString(me.Unknown))({default:K$}));let a=Object.entries(s).filter(([u])=>!Dee.has(u)&&!u.startsWith(\"SUPABASE_INTERNAL_\")),i=!(yield*ate(o));return yield*N.gen(function*(){let u=yield*Oa(()=>EdgeRuntime.userWorkers.create({servicePath:ste(n,o),memoryLimitMb:256,workerTimeoutMs:Number.isFinite(W$)?W$*1e3:4e5,noModuleCache:!1,noNpm:i,envVars:a,forceCreate:!0,customModuleRoot:\"\",cpuTimeSoftLimitMs:1e3,cpuTimeHardLimitMs:2e3,decoratorType:\"tc39\",maybeEntrypoint:i$(o.entrypointPath).href,context:{useReadSyncFileAPI:!0,...o.importMapPath===\"\"||o.importMapDiscoveredByRuntime?{}:{importMapPath:o.importMapPath}},staticPatterns:o.staticFiles}));return yield*Oa(()=>u.fetch(cte(e,t)))}).pipe(N.retry({times:1,while:({cause:u})=>e.body===null&&Hee(u)}),N.catchTag(\"BootstrapOperationError\",({cause:u})=>ic.error(\"[functions] worker error\",u).pipe(N.andThen(N.succeed(Zee(u))))))}).pipe(N.scoped),{signal:e.signal}).then(t=>{if(ir.isSuccess(t))return t.value;if(e.signal.aborted&&Ft.hasInterruptsOnly(t.cause))return new Response(null,{status:499});throw Ft.squash(t.cause)}),onListen:()=>{let t=Object.keys(tC).slice(0,5).map(r=>` - http://127.0.0.1:${H$}/functions/v1/${r}`);N.runSync(ic.log(`Serving functions on http://127.0.0.1:${H$}/functions/v1/${t.length?`\n${t.join(`\n`)}`:\"\"}\nUsing ${Deno.version.deno}`))},onError:()=>fc({code:N0[500],message:\"Request failed due to an internal server error\"},500)});export{Wee as RequestErrors,Yee as extractBearerToken,cte as prepareUserRequest};\n"; diff --git a/packages/stack/src/functions/serve-main-bundler.integration.test.ts b/packages/stack/src/functions/serve-main-bundler.integration.test.ts index 8cfcc8e95c..5aac0dd30d 100644 --- a/packages/stack/src/functions/serve-main-bundler.integration.test.ts +++ b/packages/stack/src/functions/serve-main-bundler.integration.test.ts @@ -245,7 +245,7 @@ describe("stack-owned functions bootstrap", () => { expect(received?.url).toBe("http://forwarded.example/hello"); expect(createOptions).toMatchObject({ memoryLimitMb: 256, - noModuleCache: true, + noModuleCache: false, noNpm: true, forceCreate: true, staticPatterns: [], diff --git a/packages/stack/src/functions/serve.main.ts b/packages/stack/src/functions/serve.main.ts index edf1378fde..a107e35a7d 100644 --- a/packages/stack/src/functions/serve.main.ts +++ b/packages/stack/src/functions/serve.main.ts @@ -460,7 +460,9 @@ Deno.serve({ workerTimeoutMs: Number.isFinite(WALLCLOCK_LIMIT_SEC) ? WALLCLOCK_LIMIT_SEC * 1000 : 400_000, - noModuleCache: true, + // Edge Runtime's reload mode re-extracts npm packages in place in the shared Deno cache, + // where concurrent worker boots race on the same files. + noModuleCache: false, noNpm, envVars, forceCreate: true, diff --git a/packages/stack/src/services/Functions.integration.test.ts b/packages/stack/src/services/Functions.integration.test.ts index 4686a62337..25aa3e351f 100644 --- a/packages/stack/src/services/Functions.integration.test.ts +++ b/packages/stack/src/services/Functions.integration.test.ts @@ -18,6 +18,7 @@ import { HttpClient, HttpClientRequest } from "effect/http"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; import type { FunctionsBootstrapOwner } from "../functions/FunctionsBootstrap.ts"; import { ContainerError, type ContainerRuntime } from "../runtime/Container.ts"; +import { makeFakeNpmRegistry } from "../../tests/fake-npm-registry.ts"; import { makeStandaloneService } from "../../tests/standalone-service.ts"; import { makeServiceRecipe } from "./Catalog.ts"; import * as Functions from "./Functions.ts"; @@ -596,6 +597,103 @@ for (const runtime of ["native", testEngine] as const) { ); } +it.live( + "reuses a cached npm dependency when several function workers boot at once", + () => + Effect.scoped( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const client = yield* HttpClient.HttpClient; + const probe = "cache-probe-1.0.0"; + const registry = yield* makeFakeNpmRegistry({ + name: "cache-probe", + version: "1.0.0", + source: `export const probe = "${probe}";`, + }); + const temporaryRoot = `${process.cwd()}/tmp`; + yield* fs.makeDirectory(temporaryRoot, { recursive: true }); + const root = yield* fs.makeTempDirectoryScoped({ + directory: temporaryRoot, + prefix: "functions-npm-cache-", + }); + const stackRoot = yield* fs.makeTempDirectoryScoped({ + directory: temporaryRoot, + prefix: "functions-npm-cache-stack-", + }); + const functionNames = ["alpha", "beta"]; + const functionsRoot = `${root}/supabase/functions`; + for (const name of functionNames) { + yield* fs.makeDirectory(`${functionsRoot}/${name}`, { recursive: true }); + yield* fs.writeFileString( + `${functionsRoot}/${name}/index.ts`, + 'import { probe } from "npm:cache-probe@1.0.0"; Deno.serve(() => new Response(probe));', + ); + } + const recipe = yield* makeServiceRecipe( + { + service: "functions", + pin: selection.functions, + config: { + functionsRoot, + verifyJwt: false, + env: { NPM_CONFIG_REGISTRY: registry.url }, + }, + }, + { + ...options(stackRoot), + stackId: "d".repeat(64), + instanceId: "npm-cache", + cacheRoot: testArtifactCacheRoot, + }, + ); + const logs = yield* Ref.make(""); + yield* Stream.fromSubscription(yield* recipe.logs).pipe( + Stream.runForEach(({ bytes }) => + Ref.update(logs, (text) => text + new TextDecoder().decode(bytes)), + ), + Effect.forkScoped({ startImmediately: true }), + ); + const instance = yield* makeStandaloneService(recipe.definition, { + id: "npm-cache", + config: recipe.creation, + }); + yield* Effect.gen(function* () { + yield* instance.start; + yield* instance.ready; + const endpoint = yield* recipe.endpoint("http"); + const base = `http://${httpHost(endpoint)}:${endpoint.port}`; + const call = (name: string) => + client + .get(`${base}/${name}`) + .pipe( + Effect.flatMap((response) => + response.text.pipe(Effect.map((body) => ({ status: response.status, body }))), + ), + ); + + expect(yield* call("alpha")).toEqual({ status: 200, body: probe }); + expect(yield* registry.tarballFetches).toBe(1); + + const burst = Array.from({ length: 4 }, () => functionNames).flat(); + const responses = yield* Effect.forEach(burst, call, { concurrency: "unbounded" }); + + expect({ responses, tarballFetches: yield* registry.tarballFetches }).toEqual({ + responses: burst.map(() => ({ status: 200, body: probe })), + tarballFetches: 1, + }); + yield* instance.stop; + }).pipe( + Effect.tapCause(() => + Ref.get(logs).pipe( + Effect.flatMap((text) => Effect.logError(`Functions native logs:\n${text}`)), + ), + ), + ); + }), + ).pipe(Effect.provide(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp))), + { timeout: 120_000 }, +); + it.effect("passes POSIX project paths to a docker Functions container from a Windows host", () => Effect.scoped( Effect.gen(function* () { diff --git a/packages/stack/tests/fake-npm-registry.ts b/packages/stack/tests/fake-npm-registry.ts new file mode 100644 index 0000000000..332abf3e88 --- /dev/null +++ b/packages/stack/tests/fake-npm-registry.ts @@ -0,0 +1,105 @@ +import { createHash } from "node:crypto"; +import { createServer } from "node:http"; // oxlint-disable-line effecttsgo/node-builtin-import -- real socket fixture. +import { gzipSync } from "node:zlib"; +import { Effect } from "effect"; + +const tarFile = (name: string, content: string): Uint8Array => { + const body = new TextEncoder().encode(content); + const bytes = new Uint8Array(512 + Math.ceil(body.length / 512) * 512); + const header = bytes.subarray(0, 512); + header.set(new TextEncoder().encode(name), 0); + header.set(new TextEncoder().encode("0000644\0"), 100); + header.set(new TextEncoder().encode("0000000\0"), 108); + header.set(new TextEncoder().encode("0000000\0"), 116); + header.set(new TextEncoder().encode(`${body.length.toString(8).padStart(11, "0")}\0`), 124); + header[156] = 48; + header.set(new TextEncoder().encode("ustar\0"), 257); + header.fill(32, 148, 156); + const checksum = header + .reduce((sum, value) => sum + value, 0) + .toString(8) + .padStart(6, "0"); + header.set(new TextEncoder().encode(`${checksum}\0 `), 148); + bytes.set(body, 512); + return bytes; +}; + +const packageTarball = (name: string, version: string, source: string): Uint8Array => { + const manifest = JSON.stringify({ name, version, type: "module", main: "index.js" }); + const files = [ + tarFile("package/package.json", manifest), + tarFile("package/index.js", source), + new Uint8Array(1024), + ]; + return gzipSync(Buffer.concat(files)); +}; + +/** + * A loopback npm registry serving one ESM package, `source` as its `index.js`, and counting how + * many times its tarball is downloaded. + */ +export const makeFakeNpmRegistry = (pkg: { + readonly name: string; + readonly version: string; + readonly source: string; +}) => + Effect.acquireRelease( + Effect.callback<{ + readonly url: string; + readonly server: ReturnType; + readonly counts: { tarballs: number }; + }>((resume) => { + const tarball = packageTarball(pkg.name, pkg.version, pkg.source); + const shasum = createHash("sha1").update(tarball).digest("hex"); + const integrity = `sha512-${createHash("sha512").update(tarball).digest("base64")}`; + const tarballPath = `/${pkg.name}/-/${pkg.name}-${pkg.version}.tgz`; + const counts = { tarballs: 0 }; + const server = createServer((request, response) => { + const path = new URL(request.url ?? "/", "http://registry").pathname; + if (path === `/${pkg.name}`) { + response.setHeader("content-type", "application/json"); + response.end( + JSON.stringify({ + name: pkg.name, + "dist-tags": { latest: pkg.version }, + versions: { + [pkg.version]: { + name: pkg.name, + version: pkg.version, + dist: { + tarball: `http://${request.headers.host}${tarballPath}`, + shasum, + integrity, + }, + }, + }, + }), + ); + } else if (path === tarballPath) { + counts.tarballs++; + response.setHeader("content-type", "application/octet-stream"); + response.end(tarball); + } else { + response.statusCode = 404; + response.end(); + } + }); + server.once("error", (error) => resume(Effect.die(error))); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (address === null || typeof address === "string") + return resume(Effect.die("fake npm registry has no address")); + resume(Effect.succeed({ url: `http://127.0.0.1:${address.port}/`, server, counts })); + }); + }), + ({ server }) => + Effect.callback((resume) => { + server.closeAllConnections(); + server.close(() => resume(Effect.void)); + }), + ).pipe( + Effect.map(({ url, counts }) => ({ + url, + tarballFetches: Effect.sync(() => counts.tarballs), + })), + ); From 3507dba692a6622042860f08dce60f35690707ca Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 27 Oct 2026 14:32:18 +0000 Subject: [PATCH 30/33] feat(config): accept env() on number, boolean, enum and list fields in the config schema (#7333) ## Summary The config loader resolves `env(VAR)` on any field and then coerces it to the field's type, but the published `config.schema.json` typed number, integer, boolean, enum and string-list fields strictly. So `max_rows = "env(MAX_ROWS)"` loads fine but shows as an error in the editor. Those fields now accept either their usual value or a string matching the loader's `env()` syntax. A mistyped literal like `max_rows = "1000"` or `pool_mode = "statement"` still fails. - `toPublishedJsonSchemaDocument` takes an `acceptEnvReferences` option, and the CLI config schema turns it on. Each affected field becomes an `anyOf` of its original type and an `env()` string, with `description`, `default` and `examples` kept on the outer node so hover docs look the same - String lists such as `auth.additional_redirect_urls` accept an `env()` string in place of the whole list, since the CLI splits the resolved value on commas. Lists of enums don't, as the loader doesn't split those - Fields that are already a union (e.g. `storage.file_size_limit`) get one extra `env()` branch rather than a nested one - `project-config.schema.json` doesn't change, since it describes values after `env()` has been resolved - `apps/docs/public/cli/config.schema.json` is regenerated - `ajv` is added as a dev dependency of `@supabase/config` so tests can check the published schema against what the loader accepts ## Schema shape change If you generate types or other tooling from `config.schema.json`, the affected fields now read as a union: their `type` moves from the field itself into the first `anyOf` branch, next to the `env()` branch. Configs that validated before still validate. ## Linked issue Closes CLI-2857 --- apps/docs/public/cli/config.schema.json | 4764 ++++++++++++++--- packages/config/AGENTS.md | 2 +- packages/config/package.json | 1 + .../scripts/build-artifacts.unit.test.ts | 6 +- packages/config/src/base.ts | 5 +- packages/config/src/json-schema-document.ts | 86 +- .../src/json-schema-document.unit.test.ts | 564 +- pnpm-lock.yaml | 3 + 8 files changed, 4575 insertions(+), 856 deletions(-) diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 87a9942059..e64d6aeb24 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -13,24 +13,56 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Logflare service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to the local Logflare service.", - "default": 54327 + "default": 54327, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "backend": { - "type": "string", - "enum": ["postgres", "bigquery"], "description": "Configure one of the supported backends:\n\n- `postgres`\n- `bigquery`", - "default": "postgres" + "default": "postgres", + "anyOf": [ + { + "type": "string", + "enum": ["postgres", "bigquery"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "vector_port": { - "type": "number", - "description": "Port to the local syslog ingest service." + "description": "Port to the local syslog ingest service.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "gcp_project_id": { "type": "string", @@ -51,47 +83,103 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local PostgREST service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the API URL.", - "default": 54321 + "default": 54321, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "schemas": { - "type": "array", - "items": { - "type": "string", - "description": "Schemas to expose in your API. Tables, views and stored procedures in this schema will get API endpoints." - }, - "default": ["public", "graphql_public"] + "default": ["public", "graphql_public"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Schemas to expose in your API. Tables, views and stored procedures in this schema will get API endpoints." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "extra_search_path": { - "type": "array", - "items": { - "type": "string", - "description": "Extra schemas to add to the search_path of every request." - }, - "default": ["public", "extensions"] + "default": ["public", "extensions"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Extra schemas to add to the search_path of every request." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_rows": { - "type": "number", "description": "The maximum number of rows returned from a view, table, or stored procedure. Limits payload size for accidental or malicious requests.", - "default": 1000 + "default": 1000, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "auto_expose_new_tables": { - "type": "boolean", - "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off." + "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "tls": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable HTTPS endpoints locally using a self-signed certificate.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "cert_path": { "type": "string", @@ -115,9 +203,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local GoTrue service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "site_url": { "type": "string", @@ -125,18 +221,34 @@ "default": "http://127.0.0.1:3000" }, "additional_redirect_urls": { - "type": "array", - "items": { - "type": "string", - "description": "A URL that auth providers are permitted to redirect to." - }, "description": "A list of exact URLs that auth providers are permitted to redirect to post authentication.", - "default": ["https://127.0.0.1:3000"] + "default": ["https://127.0.0.1:3000"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "A URL that auth providers are permitted to redirect to." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "jwt_expiry": { - "type": "number", "description": "How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).", - "default": 3600 + "default": 3600, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "jwt_issuer": { "type": "string", @@ -147,45 +259,101 @@ "description": "Path to the JWT signing keys file." }, "enable_refresh_token_rotation": { - "type": "boolean", "description": "If disabled, the refresh token will never expire.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "refresh_token_reuse_interval": { - "type": "number", "description": "Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_manual_linking": { - "type": "boolean", "description": "Allow/disallow testing manual linking of accounts.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups to your project.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_anonymous_sign_ins": { - "type": "boolean", "description": "Allow/disallow anonymous sign-ins to your project.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "minimum_password_length": { - "type": "number", "description": "Passwords shorter than this value will be rejected as weak.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "password_requirements": { - "type": "string", - "enum": [ - "", - "letters_digits", - "lower_upper_letters_digits", - "lower_upper_letters_digits_symbols" - ], "description": "Password character requirements.", - "default": "" + "default": "", + "anyOf": [ + { + "type": "string", + "enum": [ + "", + "letters_digits", + "lower_upper_letters_digits", + "lower_upper_letters_digits_symbols" + ] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "publishable_key": { "type": "string", @@ -216,39 +384,95 @@ "type": "object", "properties": { "email_sent": { - "type": "number", "description": "Number of emails that can be sent per hour.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sms_sent": { - "type": "number", "description": "Number of SMS messages that can be sent per hour.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "anonymous_users": { - "type": "number", "description": "Number of anonymous sign-ins that can be made per hour per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "token_refresh": { - "type": "number", "description": "Number of sessions that can be refreshed in a 5 minute interval per IP address.", - "default": 150 + "default": 150, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sign_in_sign_ups": { - "type": "number", "description": "Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "token_verifications": { - "type": "number", "description": "Number of OTP or magic link verifications that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "web3": { - "type": "number", "description": "Number of Web3 logins that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -257,14 +481,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable CAPTCHA verification.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "provider": { - "type": "string", - "enum": ["hcaptcha", "turnstile"], - "description": "CAPTCHA provider to use." + "description": "CAPTCHA provider to use.", + "anyOf": [ + { + "type": "string", + "enum": ["hcaptcha", "turnstile"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secret": { "type": "string", @@ -281,9 +521,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the mfa verification hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -301,9 +549,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the password verification hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -321,9 +577,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the custom access token hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -341,9 +605,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the send sms hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -361,9 +633,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the send email hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -381,9 +661,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the before user created hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -407,14 +695,30 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow TOTP enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow TOTP verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -423,19 +727,43 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow phone enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow phone verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_length": { - "type": "number", "description": "The length of the OTP code.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "template": { "type": "string", @@ -454,22 +782,46 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow WebAuthn enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow WebAuthn verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false }, "max_enrolled_factors": { - "type": "number", "description": "The maximum number of MFA factors a user can enroll in.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -493,24 +845,56 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via email to your project.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "double_confirm_changes": { - "type": "boolean", "description": "If enabled, a user will be required to confirm any email change on both the old and new email addresses.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_confirmations": { - "type": "boolean", "description": "If enabled, users need to confirm their email address before signing in.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secure_password_change": { - "type": "boolean", "description": "If enabled, users will need to reauthenticate or have logged in recently to change their password.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_frequency": { "type": "string", @@ -518,30 +902,62 @@ "default": "1s" }, "otp_length": { - "type": "number", "description": "Number of characters used in the email OTP.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_expiry": { - "type": "number", "description": "Number of seconds before the email OTP expires.", - "default": 3600 + "default": 3600, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "smtp": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable SMTP for email delivery.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "host": { "type": "string", "description": "Hostname or IP address of the SMTP server." }, "port": { - "type": "number", - "description": "Port number of the SMTP server." + "description": "Port number of the SMTP server.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "user": { "type": "string", @@ -608,9 +1024,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the notification email.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "subject": { "type": "string", @@ -645,14 +1069,30 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via SMS to your project.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_confirmations": { - "type": "boolean", "description": "If enabled, users need to confirm their phone number before signing in.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "template": { "type": "string", @@ -665,22 +1105,46 @@ "default": "5s" }, "otp_length": { - "type": "number", "description": "Number of characters used in the SMS OTP.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_expiry": { - "type": "number", "description": "Number of seconds before the SMS OTP expires.", - "default": 60 + "default": 60, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "twilio": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Twilio provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "account_sid": { "type": "string", @@ -709,9 +1173,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Twilio Verify provider for phone verification.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "account_sid": { "type": "string", @@ -733,9 +1205,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable MessageBird provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "originator": { "type": "string", @@ -753,9 +1233,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Textlocal provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sender": { "type": "string", @@ -773,9 +1261,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Vonage provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "from": { "type": "string", @@ -810,9 +1306,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Apple OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -836,14 +1340,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -852,9 +1372,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Azure OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -878,14 +1406,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -894,9 +1438,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Bitbucket OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -920,14 +1472,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -936,9 +1504,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Discord OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -962,14 +1538,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -978,9 +1570,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Facebook OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1004,14 +1604,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1020,9 +1636,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Figma OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1046,14 +1670,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1062,9 +1702,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the GitHub OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1088,14 +1736,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1104,9 +1768,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the GitLab OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1130,14 +1802,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1146,9 +1834,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Google OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1172,14 +1868,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1188,9 +1900,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Kakao OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1214,14 +1934,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1230,9 +1966,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Keycloak OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1257,14 +2001,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1273,9 +2033,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the LinkedIn OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1299,14 +2067,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1315,9 +2099,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Notion OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1341,14 +2133,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1357,9 +2165,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Twitch OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1383,14 +2199,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1399,9 +2231,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Twitter OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1425,14 +2265,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1441,9 +2297,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the X OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1467,14 +2331,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1483,9 +2363,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Slack OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1509,14 +2397,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1525,9 +2429,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Spotify OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1551,14 +2463,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1567,9 +2495,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the WorkOS OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1593,14 +2529,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1609,9 +2561,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Zoom OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1635,14 +2595,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1657,9 +2633,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this Web3 provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1668,9 +2652,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this Web3 provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1682,9 +2674,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable OAuth server functionality.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "authorization_url_path": { "type": "string", @@ -1692,9 +2692,17 @@ "default": "/oauth/consent" }, "allow_dynamic_registration": { - "type": "boolean", "description": "Allow dynamic client registration.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1706,9 +2714,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "project_id": { "type": "string", @@ -1721,9 +2737,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "tenant": { "type": "string", @@ -1740,9 +2764,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "user_pool_id": { "type": "string", @@ -1759,9 +2791,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "domain": { "type": "string", @@ -1774,9 +2814,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "issuer_url": { "type": "string", @@ -1795,14 +2843,30 @@ "type": "object", "properties": { "port": { - "type": "number", "description": "Port to use for the local database URL.", - "default": 54322 + "default": 54322, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "shadow_port": { - "type": "number", "description": "Port used by db diff command to initialize the shadow database.", - "default": 54320 + "default": 54320, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "health_timeout": { "type": "string", @@ -1810,9 +2874,17 @@ "default": "2m" }, "major_version": { - "type": "number", "description": "The database major version to use. This has to be the same as your remote database's.", - "default": 17 + "default": 17, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "orioledb_version": { "type": "string", @@ -1822,30 +2894,70 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local PgBouncer service.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the local connection pooler.", - "default": 54329 + "default": 54329, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "pool_mode": { - "type": "string", - "enum": ["transaction", "session"], "description": "Specifies when a server connection can be reused by other clients.", - "default": "transaction" + "default": "transaction", + "anyOf": [ + { + "type": "string", + "enum": ["transaction", "session"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "default_pool_size": { - "type": "number", "description": "How many server connections to allow per user/database pair.", - "default": 20 + "default": 20, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_client_conn": { - "type": "number", "description": "Maximum number of client connections allowed.", - "default": 100 + "default": 100, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1854,18 +2966,34 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "If disabled, migrations will be skipped during a db push or reset.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "schema_paths": { - "type": "array", - "items": { - "type": "string", - "description": "Schema file path, directory, or glob relative to the supabase directory." - }, "description": "Ordered list of schema files, directories, or glob patterns that describe your database.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Schema file path, directory, or glob relative to the supabase directory." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1874,18 +3002,34 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable seeding the database with SQL files.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sql_paths": { - "type": "array", - "items": { - "type": "string", - "description": "Path to a SQL file used to seed the database." - }, "description": "Ordered list of seed files to load during db reset.", - "default": ["./seed.sql"] + "default": ["./seed.sql"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Path to a SQL file used to seed the database." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1903,22 +3047,70 @@ "type": "string" }, "max_connections": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_locks_per_transaction": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_maintenance_workers": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_workers": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_workers_per_gather": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_replication_slots": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_slot_wal_keep_size": { "type": "string" @@ -1933,15 +3125,39 @@ "type": "string" }, "max_wal_senders": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_worker_processes": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "session_replication_role": { - "type": "string", - "enum": ["origin", "replica", "local"], - "description": "Session replication role." + "description": "Session replication role.", + "anyOf": [ + { + "type": "string", + "enum": ["origin", "replica", "local"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "shared_buffers": { "type": "string" @@ -1953,7 +3169,15 @@ "type": "string" }, "track_commit_timestamp": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "wal_keep_size": { "type": "string" @@ -1971,25 +3195,49 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable management of network restrictions.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "allowed_cidrs": { - "type": "array", - "items": { - "type": "string" - }, "description": "Allowed IPv4 CIDR blocks.", - "default": ["0.0.0.0/0"] + "default": ["0.0.0.0/0"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "allowed_cidrs_v6": { - "type": "array", - "items": { - "type": "string" - }, "description": "Allowed IPv6 CIDR blocks.", - "default": ["::/0"] + "default": ["::/0"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1998,9 +3246,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Reject non-secure connections to the database.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2021,25 +3277,57 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Edge Runtime service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "policy": { - "type": "string", - "enum": ["oneshot", "per_worker"], "description": "Configure the supported request policy.", - "default": "per_worker" + "default": "per_worker", + "anyOf": [ + { + "type": "string", + "enum": ["oneshot", "per_worker"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "inspector_port": { - "type": "number", "description": "Port to run the Edge Functions inspector on.", - "default": 8083 + "default": 8083, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "deno_version": { - "type": "number", "description": "The Deno major version to use.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secrets": { "type": "object", @@ -2064,14 +3352,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Controls whether a function is deployed or served.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_jwt": { - "type": "boolean", "description": "By default, deployed or locally served functions reject requests without a valid JWT.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "import_map": { "type": "string", @@ -2084,13 +3388,21 @@ "default": "" }, "static_files": { - "type": "array", - "items": { - "type": "string", - "description": "Static file glob for the function." - }, "description": "Static files to bundle with the function.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Static file glob for the function." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "env": { "type": "object", @@ -2125,22 +3437,54 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local SMTP testing server.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the email testing server web interface.\n\nEmails sent with the local dev setup are monitored and available from the web interface.", - "default": 54324 + "default": 54324, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "smtp_port": { - "type": "number", - "description": "Optional SMTP port to expose for local testing." + "description": "Optional SMTP port to expose for local testing.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "pop3_port": { - "type": "number", - "description": "Optional POP3 port to expose for local testing." + "description": "Optional POP3 port to expose for local testing.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "admin_email": { "type": "string", @@ -2157,20 +3501,44 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Realtime service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "ip_version": { - "type": "string", - "enum": ["IPv4", "IPv6"], "description": "Bind realtime via either IPv4 or IPv6.", - "default": "IPv4" + "default": "IPv4", + "anyOf": [ + { + "type": "string", + "enum": ["IPv4", "IPv6"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_header_length": { - "type": "number", "description": "Maximum length of the HTTP header.", - "default": 4096 + "default": 4096, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2179,9 +3547,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Storage service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "file_size_limit": { "anyOf": [ @@ -2190,6 +3566,10 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, @@ -2197,9 +3577,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable image transformation.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2212,9 +3600,17 @@ "type": "object", "properties": { "public": { - "type": "boolean", "description": "Enable public access to the bucket.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "file_size_limit": { "anyOf": [ @@ -2223,17 +3619,29 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, "allowed_mime_types": { - "type": "array", - "items": { - "type": "string", - "description": "A MIME type allowed for the bucket." - }, "description": "The list of allowed MIME types for the bucket.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "A MIME type allowed for the bucket." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "objects_path": { "type": "string", @@ -2254,9 +3662,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Allow connections via S3 compatible clients.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2265,24 +3681,56 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable analytics buckets.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_namespaces": { - "type": "number", "description": "Maximum number of analytics namespaces.", - "default": 5 + "default": 5, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_tables": { - "type": "number", "description": "Maximum number of analytics tables.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_catalogs": { - "type": "number", "description": "Maximum number of analytics catalogs.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "buckets": { "anyOf": [ @@ -2320,19 +3768,43 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable vector buckets.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_buckets": { - "type": "number", "description": "Maximum number of vector buckets.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_indexes": { - "type": "number", "description": "Maximum number of vector indexes.", - "default": 5 + "default": 5, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "buckets": { "anyOf": [ @@ -2373,14 +3845,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Supabase Studio dashboard.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for Supabase Studio.", - "default": 54323 + "default": 54323, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "api_url": { "type": "string", @@ -2420,10 +3908,18 @@ "examples": ["private"] }, "instances": { - "type": "integer", - "minimum": 0, "description": "Number of instances to run. Every deploy sends a complete spec, so a count\nrecorded here is what keeps a scaled compute scaled; `--instances` overrides\nit for one deploy. Defaults to 1.", - "examples": [3] + "examples": [3], + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "source": { "type": "string", @@ -2431,12 +3927,20 @@ "examples": ["packages/api"] }, "exclude": { - "type": "array", - "items": { - "type": "string" - }, "description": "Patterns for paths to leave out of the uploaded build context, read the way\n`.gitignore` reads them: a pattern without `/` matches that name at any depth,\none with `/` is anchored at the compute's source directory, a trailing `/`\nmatches directories only, and `**` spans directories. Excluding a directory\nexcludes everything under it; re-inclusion (`!`) is not supported. Only the\npatterns recorded here are excluded, so a compute with no list uploads its source\ndirectory whole.", - "examples": [["node_modules", ".env"]] + "examples": [["node_modules", ".env"]], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2454,12 +3958,28 @@ "type": "object", "properties": { "compute": { - "type": "boolean", - "description": "Enable the experimental compute command family." + "description": "Enable the experimental compute command family.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "stack": { - "type": "boolean", - "description": "Use the new local stack backend for start, stop, and status, and for --local targets of the db, migration, test db, gen types, inspect, and pull command families." + "description": "Use the new local stack backend for start, stop, and status, and for --local targets of the db, migration, test db, gen types, inspect, and pull command families.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "orioledb_version": { "type": "string", @@ -2491,9 +4011,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable experimental webhooks.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2502,9 +4030,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "declarative_schema_path": { "type": "string", @@ -2745,24 +4281,56 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Logflare service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to the local Logflare service.", - "default": 54327 + "default": 54327, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "backend": { - "type": "string", - "enum": ["postgres", "bigquery"], "description": "Configure one of the supported backends:\n\n- `postgres`\n- `bigquery`", - "default": "postgres" + "default": "postgres", + "anyOf": [ + { + "type": "string", + "enum": ["postgres", "bigquery"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "vector_port": { - "type": "number", - "description": "Port to the local syslog ingest service." + "description": "Port to the local syslog ingest service.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "gcp_project_id": { "type": "string", @@ -2783,47 +4351,103 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local PostgREST service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the API URL.", - "default": 54321 + "default": 54321, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "schemas": { - "type": "array", - "items": { - "type": "string", - "description": "Schemas to expose in your API. Tables, views and stored procedures in this schema will get API endpoints." - }, - "default": ["public", "graphql_public"] + "default": ["public", "graphql_public"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Schemas to expose in your API. Tables, views and stored procedures in this schema will get API endpoints." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "extra_search_path": { - "type": "array", - "items": { - "type": "string", - "description": "Extra schemas to add to the search_path of every request." - }, - "default": ["public", "extensions"] + "default": ["public", "extensions"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Extra schemas to add to the search_path of every request." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_rows": { - "type": "number", "description": "The maximum number of rows returned from a view, table, or stored procedure. Limits payload size for accidental or malicious requests.", - "default": 1000 + "default": 1000, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "auto_expose_new_tables": { - "type": "boolean", - "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off." + "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "tls": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable HTTPS endpoints locally using a self-signed certificate.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "cert_path": { "type": "string", @@ -2847,9 +4471,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local GoTrue service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "site_url": { "type": "string", @@ -2857,18 +4489,34 @@ "default": "http://127.0.0.1:3000" }, "additional_redirect_urls": { - "type": "array", - "items": { - "type": "string", - "description": "A URL that auth providers are permitted to redirect to." - }, "description": "A list of exact URLs that auth providers are permitted to redirect to post authentication.", - "default": ["https://127.0.0.1:3000"] + "default": ["https://127.0.0.1:3000"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "A URL that auth providers are permitted to redirect to." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "jwt_expiry": { - "type": "number", "description": "How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).", - "default": 3600 + "default": 3600, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "jwt_issuer": { "type": "string", @@ -2879,45 +4527,101 @@ "description": "Path to the JWT signing keys file." }, "enable_refresh_token_rotation": { - "type": "boolean", "description": "If disabled, the refresh token will never expire.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "refresh_token_reuse_interval": { - "type": "number", "description": "Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_manual_linking": { - "type": "boolean", "description": "Allow/disallow testing manual linking of accounts.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups to your project.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_anonymous_sign_ins": { - "type": "boolean", "description": "Allow/disallow anonymous sign-ins to your project.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "minimum_password_length": { - "type": "number", "description": "Passwords shorter than this value will be rejected as weak.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "password_requirements": { - "type": "string", - "enum": [ - "", - "letters_digits", - "lower_upper_letters_digits", - "lower_upper_letters_digits_symbols" - ], "description": "Password character requirements.", - "default": "" + "default": "", + "anyOf": [ + { + "type": "string", + "enum": [ + "", + "letters_digits", + "lower_upper_letters_digits", + "lower_upper_letters_digits_symbols" + ] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "publishable_key": { "type": "string", @@ -2948,39 +4652,95 @@ "type": "object", "properties": { "email_sent": { - "type": "number", "description": "Number of emails that can be sent per hour.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sms_sent": { - "type": "number", "description": "Number of SMS messages that can be sent per hour.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "anonymous_users": { - "type": "number", "description": "Number of anonymous sign-ins that can be made per hour per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "token_refresh": { - "type": "number", "description": "Number of sessions that can be refreshed in a 5 minute interval per IP address.", - "default": 150 + "default": 150, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sign_in_sign_ups": { - "type": "number", "description": "Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "token_verifications": { - "type": "number", "description": "Number of OTP or magic link verifications that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "web3": { - "type": "number", "description": "Number of Web3 logins that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2989,14 +4749,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable CAPTCHA verification.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "provider": { - "type": "string", - "enum": ["hcaptcha", "turnstile"], - "description": "CAPTCHA provider to use." + "description": "CAPTCHA provider to use.", + "anyOf": [ + { + "type": "string", + "enum": ["hcaptcha", "turnstile"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secret": { "type": "string", @@ -3013,9 +4789,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the mfa verification hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3033,9 +4817,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the password verification hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3053,9 +4845,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the custom access token hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3073,9 +4873,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the send sms hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3093,9 +4901,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the send email hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3113,9 +4929,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the before user created hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3139,14 +4963,30 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow TOTP enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow TOTP verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3155,19 +4995,43 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow phone enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow phone verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_length": { - "type": "number", "description": "The length of the OTP code.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "template": { "type": "string", @@ -3186,22 +5050,46 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow WebAuthn enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow WebAuthn verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false }, "max_enrolled_factors": { - "type": "number", "description": "The maximum number of MFA factors a user can enroll in.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3225,24 +5113,56 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via email to your project.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "double_confirm_changes": { - "type": "boolean", "description": "If enabled, a user will be required to confirm any email change on both the old and new email addresses.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_confirmations": { - "type": "boolean", "description": "If enabled, users need to confirm their email address before signing in.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secure_password_change": { - "type": "boolean", "description": "If enabled, users will need to reauthenticate or have logged in recently to change their password.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_frequency": { "type": "string", @@ -3250,30 +5170,62 @@ "default": "1s" }, "otp_length": { - "type": "number", "description": "Number of characters used in the email OTP.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_expiry": { - "type": "number", "description": "Number of seconds before the email OTP expires.", - "default": 3600 + "default": 3600, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "smtp": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable SMTP for email delivery.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "host": { "type": "string", "description": "Hostname or IP address of the SMTP server." }, "port": { - "type": "number", - "description": "Port number of the SMTP server." + "description": "Port number of the SMTP server.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "user": { "type": "string", @@ -3340,9 +5292,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the notification email.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "subject": { "type": "string", @@ -3377,14 +5337,30 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via SMS to your project.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_confirmations": { - "type": "boolean", "description": "If enabled, users need to confirm their phone number before signing in.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "template": { "type": "string", @@ -3397,22 +5373,46 @@ "default": "5s" }, "otp_length": { - "type": "number", "description": "Number of characters used in the SMS OTP.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_expiry": { - "type": "number", "description": "Number of seconds before the SMS OTP expires.", - "default": 60 + "default": 60, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "twilio": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Twilio provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "account_sid": { "type": "string", @@ -3441,9 +5441,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Twilio Verify provider for phone verification.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "account_sid": { "type": "string", @@ -3465,9 +5473,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable MessageBird provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "originator": { "type": "string", @@ -3485,9 +5501,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Textlocal provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sender": { "type": "string", @@ -3505,9 +5529,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Vonage provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "from": { "type": "string", @@ -3542,9 +5574,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Apple OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3568,14 +5608,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3584,9 +5640,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Azure OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3610,14 +5674,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3626,9 +5706,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Bitbucket OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3652,14 +5740,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3668,9 +5772,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Discord OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3694,14 +5806,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3710,9 +5838,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Facebook OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3736,14 +5872,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3752,9 +5904,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Figma OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3778,14 +5938,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3794,9 +5970,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the GitHub OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3820,14 +6004,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3836,9 +6036,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the GitLab OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3862,14 +6070,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3878,9 +6102,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Google OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3904,14 +6136,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3920,9 +6168,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Kakao OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3946,14 +6202,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3962,9 +6234,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Keycloak OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3989,14 +6269,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4005,9 +6301,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the LinkedIn OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4031,14 +6335,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4047,9 +6367,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Notion OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4073,14 +6401,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4089,9 +6433,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Twitch OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4115,14 +6467,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4131,9 +6499,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Twitter OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4157,14 +6533,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4173,9 +6565,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the X OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4199,14 +6599,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4215,9 +6631,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Slack OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4241,14 +6665,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4257,9 +6697,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Spotify OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4283,14 +6731,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4299,9 +6763,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the WorkOS OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4325,14 +6797,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4341,9 +6829,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Zoom OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4367,14 +6863,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4389,9 +6901,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this Web3 provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4400,9 +6920,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this Web3 provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4414,9 +6942,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable OAuth server functionality.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "authorization_url_path": { "type": "string", @@ -4424,9 +6960,17 @@ "default": "/oauth/consent" }, "allow_dynamic_registration": { - "type": "boolean", "description": "Allow dynamic client registration.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4438,9 +6982,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "project_id": { "type": "string", @@ -4453,9 +7005,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "tenant": { "type": "string", @@ -4472,9 +7032,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "user_pool_id": { "type": "string", @@ -4491,9 +7059,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "domain": { "type": "string", @@ -4506,9 +7082,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "issuer_url": { "type": "string", @@ -4527,14 +7111,30 @@ "type": "object", "properties": { "port": { - "type": "number", "description": "Port to use for the local database URL.", - "default": 54322 + "default": 54322, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "shadow_port": { - "type": "number", "description": "Port used by db diff command to initialize the shadow database.", - "default": 54320 + "default": 54320, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "health_timeout": { "type": "string", @@ -4542,9 +7142,17 @@ "default": "2m" }, "major_version": { - "type": "number", "description": "The database major version to use. This has to be the same as your remote database's.", - "default": 17 + "default": 17, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "orioledb_version": { "type": "string", @@ -4554,30 +7162,70 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local PgBouncer service.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the local connection pooler.", - "default": 54329 + "default": 54329, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "pool_mode": { - "type": "string", - "enum": ["transaction", "session"], "description": "Specifies when a server connection can be reused by other clients.", - "default": "transaction" + "default": "transaction", + "anyOf": [ + { + "type": "string", + "enum": ["transaction", "session"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "default_pool_size": { - "type": "number", "description": "How many server connections to allow per user/database pair.", - "default": 20 + "default": 20, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_client_conn": { - "type": "number", "description": "Maximum number of client connections allowed.", - "default": 100 + "default": 100, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4586,18 +7234,34 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "If disabled, migrations will be skipped during a db push or reset.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "schema_paths": { - "type": "array", - "items": { - "type": "string", - "description": "Schema file path, directory, or glob relative to the supabase directory." - }, "description": "Ordered list of schema files, directories, or glob patterns that describe your database.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Schema file path, directory, or glob relative to the supabase directory." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4606,18 +7270,34 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable seeding the database with SQL files.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sql_paths": { - "type": "array", - "items": { - "type": "string", - "description": "Path to a SQL file used to seed the database." - }, "description": "Ordered list of seed files to load during db reset.", - "default": ["./seed.sql"] + "default": ["./seed.sql"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Path to a SQL file used to seed the database." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4635,22 +7315,70 @@ "type": "string" }, "max_connections": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_locks_per_transaction": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_maintenance_workers": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_workers": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_workers_per_gather": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_replication_slots": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_slot_wal_keep_size": { "type": "string" @@ -4665,15 +7393,39 @@ "type": "string" }, "max_wal_senders": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_worker_processes": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "session_replication_role": { - "type": "string", - "enum": ["origin", "replica", "local"], - "description": "Session replication role." + "description": "Session replication role.", + "anyOf": [ + { + "type": "string", + "enum": ["origin", "replica", "local"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "shared_buffers": { "type": "string" @@ -4685,7 +7437,15 @@ "type": "string" }, "track_commit_timestamp": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "wal_keep_size": { "type": "string" @@ -4703,25 +7463,49 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable management of network restrictions.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "allowed_cidrs": { - "type": "array", - "items": { - "type": "string" - }, "description": "Allowed IPv4 CIDR blocks.", - "default": ["0.0.0.0/0"] + "default": ["0.0.0.0/0"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "allowed_cidrs_v6": { - "type": "array", - "items": { - "type": "string" - }, "description": "Allowed IPv6 CIDR blocks.", - "default": ["::/0"] + "default": ["::/0"], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4730,9 +7514,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Reject non-secure connections to the database.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4753,25 +7545,57 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Edge Runtime service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "policy": { - "type": "string", - "enum": ["oneshot", "per_worker"], "description": "Configure the supported request policy.", - "default": "per_worker" + "default": "per_worker", + "anyOf": [ + { + "type": "string", + "enum": ["oneshot", "per_worker"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "inspector_port": { - "type": "number", "description": "Port to run the Edge Functions inspector on.", - "default": 8083 + "default": 8083, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "deno_version": { - "type": "number", "description": "The Deno major version to use.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secrets": { "type": "object", @@ -4796,14 +7620,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Controls whether a function is deployed or served.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_jwt": { - "type": "boolean", "description": "By default, deployed or locally served functions reject requests without a valid JWT.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "import_map": { "type": "string", @@ -4816,13 +7656,21 @@ "default": "" }, "static_files": { - "type": "array", - "items": { - "type": "string", - "description": "Static file glob for the function." - }, "description": "Static files to bundle with the function.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Static file glob for the function." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "env": { "type": "object", @@ -4857,22 +7705,54 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local SMTP testing server.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the email testing server web interface.\n\nEmails sent with the local dev setup are monitored and available from the web interface.", - "default": 54324 + "default": 54324, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "smtp_port": { - "type": "number", - "description": "Optional SMTP port to expose for local testing." + "description": "Optional SMTP port to expose for local testing.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "pop3_port": { - "type": "number", - "description": "Optional POP3 port to expose for local testing." + "description": "Optional POP3 port to expose for local testing.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "admin_email": { "type": "string", @@ -4889,20 +7769,44 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Realtime service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "ip_version": { - "type": "string", - "enum": ["IPv4", "IPv6"], "description": "Bind realtime via either IPv4 or IPv6.", - "default": "IPv4" + "default": "IPv4", + "anyOf": [ + { + "type": "string", + "enum": ["IPv4", "IPv6"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_header_length": { - "type": "number", "description": "Maximum length of the HTTP header.", - "default": 4096 + "default": 4096, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4911,9 +7815,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Storage service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "file_size_limit": { "anyOf": [ @@ -4922,6 +7834,10 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, @@ -4929,9 +7845,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable image transformation.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4944,9 +7868,17 @@ "type": "object", "properties": { "public": { - "type": "boolean", "description": "Enable public access to the bucket.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "file_size_limit": { "anyOf": [ @@ -4955,17 +7887,29 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, "allowed_mime_types": { - "type": "array", - "items": { - "type": "string", - "description": "A MIME type allowed for the bucket." - }, "description": "The list of allowed MIME types for the bucket.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "A MIME type allowed for the bucket." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "objects_path": { "type": "string", @@ -4986,9 +7930,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Allow connections via S3 compatible clients.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4997,24 +7949,56 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable analytics buckets.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_namespaces": { - "type": "number", "description": "Maximum number of analytics namespaces.", - "default": 5 + "default": 5, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_tables": { - "type": "number", "description": "Maximum number of analytics tables.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_catalogs": { - "type": "number", "description": "Maximum number of analytics catalogs.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "buckets": { "anyOf": [ @@ -5052,19 +8036,43 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable vector buckets.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_buckets": { - "type": "number", "description": "Maximum number of vector buckets.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_indexes": { - "type": "number", "description": "Maximum number of vector indexes.", - "default": 5 + "default": 5, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "buckets": { "anyOf": [ @@ -5105,14 +8113,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Supabase Studio dashboard.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for Supabase Studio.", - "default": 54323 + "default": 54323, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "api_url": { "type": "string", @@ -5152,10 +8176,18 @@ "examples": ["private"] }, "instances": { - "type": "integer", - "minimum": 0, "description": "Number of instances to run. Every deploy sends a complete spec, so a count\nrecorded here is what keeps a scaled compute scaled; `--instances` overrides\nit for one deploy. Defaults to 1.", - "examples": [3] + "examples": [3], + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "source": { "type": "string", @@ -5163,12 +8195,20 @@ "examples": ["packages/api"] }, "exclude": { - "type": "array", - "items": { - "type": "string" - }, "description": "Patterns for paths to leave out of the uploaded build context, read the way\n`.gitignore` reads them: a pattern without `/` matches that name at any depth,\none with `/` is anchored at the compute's source directory, a trailing `/`\nmatches directories only, and `**` spans directories. Excluding a directory\nexcludes everything under it; re-inclusion (`!`) is not supported. Only the\npatterns recorded here are excluded, so a compute with no list uploads its source\ndirectory whole.", - "examples": [["node_modules", ".env"]] + "examples": [["node_modules", ".env"]], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -5186,12 +8226,28 @@ "type": "object", "properties": { "compute": { - "type": "boolean", - "description": "Enable the experimental compute command family." + "description": "Enable the experimental compute command family.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "stack": { - "type": "boolean", - "description": "Use the new local stack backend for start, stop, and status, and for --local targets of the db, migration, test db, gen types, inspect, and pull command families." + "description": "Use the new local stack backend for start, stop, and status, and for --local targets of the db, migration, test db, gen types, inspect, and pull command families.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "orioledb_version": { "type": "string", @@ -5223,9 +8279,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable experimental webhooks.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -5234,9 +8298,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "declarative_schema_path": { "type": "string", diff --git a/packages/config/AGENTS.md b/packages/config/AGENTS.md index f1c0b4eb45..46ae61ee30 100644 --- a/packages/config/AGENTS.md +++ b/packages/config/AGENTS.md @@ -50,7 +50,7 @@ artifacts (`./schema.json`, `./project-schema.json`). `apps/cli` needs them, and internal-only keeps the published semver surface unchanged). Anything here can change or vanish in any release. - `@supabase/config/schema.json` — generated JSON Schema (draft 2020-12) for `CliConfig` (a - `dist/` build output). + `dist/` build output); number, integer, boolean, enum and string-list fields also accept `env(VAR)` strings. - `@supabase/config/project-schema.json` (CLI-2234) — generated JSON Schema (draft 2020-12) for `ProjectConfig`, derived from `ProjectConfigSchema` (`src/project-config/project-schema.ts`); a `dist/` build output alongside `schema.json`. diff --git a/packages/config/package.json b/packages/config/package.json index 8302128046..8fffa6469a 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -84,6 +84,7 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@vitest/coverage-v8": "catalog:", + "ajv": "^8.20.0", "effect": "catalog:", "semantic-release": "catalog:", "typescript": "catalog:", diff --git a/packages/config/scripts/build-artifacts.unit.test.ts b/packages/config/scripts/build-artifacts.unit.test.ts index f8da54e665..3a7a50f404 100644 --- a/packages/config/scripts/build-artifacts.unit.test.ts +++ b/packages/config/scripts/build-artifacts.unit.test.ts @@ -75,7 +75,11 @@ describe("published JSON Schema artifacts", () => { throw new Error("expected properties.api.properties.max_rows to be an object"); } const maxRows = apiProperties["max_rows"]; - expect(maxRows["type"]).toBe("number"); + const anyOf = maxRows["anyOf"]; + if (!Array.isArray(anyOf) || !isRecord(anyOf[0])) { + throw new Error("expected max_rows to be an anyOf of the number and an env() string"); + } + expect(anyOf[0]["type"]).toBe("number"); expect(typeof maxRows["description"]).toBe("string"); expect(maxRows["default"]).toBe(1000); }); diff --git a/packages/config/src/base.ts b/packages/config/src/base.ts index 143762fd7f..6f6af2e211 100644 --- a/packages/config/src/base.ts +++ b/packages/config/src/base.ts @@ -83,7 +83,10 @@ export const CliConfigSchema = Schema.Struct({ }); export function toCliConfigJsonSchema() { - return toPublishedJsonSchemaDocument(CliConfigSchema, { onExcessProperty: "error" }); + return toPublishedJsonSchemaDocument(CliConfigSchema, { + onExcessProperty: "error", + acceptEnvReferences: true, + }); } export type CliConfig = typeof CliConfigSchema.Type; diff --git a/packages/config/src/json-schema-document.ts b/packages/config/src/json-schema-document.ts index 2fbc1cbe66..9fe55f4f53 100644 --- a/packages/config/src/json-schema-document.ts +++ b/packages/config/src/json-schema-document.ts @@ -1,23 +1,38 @@ import { Schema } from "effect"; +import { ENV_PATTERN } from "./lib/env.ts"; + +type Node = Record; + +const COERCED_TYPES = new Set(["number", "integer", "boolean"]); +const HOISTED_ANNOTATIONS = new Set(["title", "description", "default", "examples"]); +const DATA_KEYWORDS = new Set(["default", "examples", "enum", "const"]); +const SCHEMA_MAP_KEYWORDS = new Set(["properties", "patternProperties"]); /** * Renders a published JSON Schema document, carrying the `x-secret` marker through and keeping * pattern-keyed records open: the generator writes a boolean `additionalProperties` on them * (effect 4.0) with no per-schema setting, while the published documents never carried one there. + * With `acceptEnvReferences`, number, integer, boolean, string-enum and string-list leaves also + * accept the `env(VAR)` references the loader resolves. */ export function toPublishedJsonSchemaDocument( schema: Schema.Constraint, - options: { readonly onExcessProperty: "ignore" | "error" }, + options: { + readonly onExcessProperty: "ignore" | "error"; + readonly acceptEnvReferences?: boolean; + }, ) { const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: options.onExcessProperty, includeAnnotationKey: (key) => key === "x-secret", }); + const root = openPatternKeyedRecords(document.schema); + const definitions = openPatternKeyedRecords(document.definitions); return { $schema: "https://json-schema.org/draft/2020-12/schema", - ...openPatternKeyedRecords(document.schema), - ...(Object.keys(document.definitions).length > 0 - ? { $defs: openPatternKeyedRecords(document.definitions) } + ...(options.acceptEnvReferences ? acceptEnvReferences(root) : root), + ...(Object.keys(definitions).length > 0 + ? { $defs: options.acceptEnvReferences ? mapSchemas(definitions) : definitions } : {}), }; } @@ -37,3 +52,66 @@ function isRecordLike(node: Record): boolean { const patternProperties = node["patternProperties"]; return typeof patternProperties === "object" && patternProperties !== null; } + +function acceptEnvReferences(node: T): T { + if (Array.isArray(node)) return node.map(acceptEnvReferences) as T; + if (!isNode(node)) return node; + if (isEnvReferenceTarget(node)) return withEnvReferenceBranch(node) as T; + const out: Node = {}; + for (const [key, value] of Object.entries(node)) { + if (DATA_KEYWORDS.has(key)) out[key] = value; + else if (SCHEMA_MAP_KEYWORDS.has(key)) out[key] = mapSchemas(value); + else if (key === "anyOf" && Array.isArray(value)) out[key] = acceptEnvReferencesInUnion(value); + else out[key] = acceptEnvReferences(value); + } + return out as T; +} + +function mapSchemas(schemas: unknown): unknown { + if (!isNode(schemas)) return schemas; + return Object.fromEntries( + Object.entries(schemas).map(([name, schema]) => [name, acceptEnvReferences(schema)]), + ); +} + +function acceptEnvReferencesInUnion(members: ReadonlyArray): Array { + const accepted = members.map((member) => + isEnvReferenceTarget(member) ? member : acceptEnvReferences(member), + ); + const needsBranch = members.some(isEnvReferenceTarget) && !accepted.some(isEnvReferenceBranch); + return needsBranch ? [...accepted, envReferenceBranch()] : accepted; +} + +function withEnvReferenceBranch(node: Node): Node { + const outer: Node = {}; + const typed: Node = {}; + for (const [key, value] of Object.entries(node)) { + (HOISTED_ANNOTATIONS.has(key) ? outer : typed)[key] = value; + } + return { ...outer, anyOf: [typed, envReferenceBranch()] }; +} + +function envReferenceBranch(): Node { + return { type: "string", pattern: ENV_PATTERN }; +} + +function isEnvReferenceBranch(node: unknown): boolean { + if (!isNode(node) || node["type"] !== "string") return false; + const checks = Array.isArray(node["allOf"]) ? [node, ...node["allOf"]] : [node]; + return checks.some((check) => isNode(check) && check["pattern"] === ENV_PATTERN); +} + +function isEnvReferenceTarget(node: unknown): node is Node { + if (!isNode(node) || typeof node["type"] !== "string") return false; + if (COERCED_TYPES.has(node["type"])) return true; + if (node["type"] === "string") return !isFreeFormString(node); + return node["type"] === "array" && isFreeFormString(node["items"]); +} + +function isFreeFormString(node: unknown): boolean { + return isNode(node) && node["type"] === "string" && !("enum" in node); +} + +function isNode(value: unknown): value is Node { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/config/src/json-schema-document.unit.test.ts b/packages/config/src/json-schema-document.unit.test.ts index 99eba9d5ee..1e0cb4c19c 100644 --- a/packages/config/src/json-schema-document.unit.test.ts +++ b/packages/config/src/json-schema-document.unit.test.ts @@ -1,8 +1,15 @@ -import { Schema } from "effect"; +import type { ErrorObject, ValidateFunction } from "ajv"; +import Ajv2020 from "ajv/dist/2020"; +import { Result, Schema, SchemaIssue } from "effect"; import { describe, expect, test } from "vitest"; -import { toCliConfigJsonSchema } from "./base.ts"; +import { CliConfigSchema, toCliConfigJsonSchema } from "./base.ts"; import { toPublishedJsonSchemaDocument } from "./json-schema-document.ts"; -import { env } from "./lib/env.ts"; +import { + ENV_PATTERN, + env, + interpolateEnvReferencesAgainstSchema, + isEnvReference, +} from "./lib/env.ts"; import { secretPathPatterns } from "./lib/secret-paths.ts"; import { toProjectConfigJsonSchema } from "./project-config/project-schema.ts"; @@ -17,6 +24,162 @@ const at = (node: unknown, ...path: ReadonlyArray): Node => { const isNode = (value: unknown): value is Node => typeof value === "object" && value !== null && !Array.isArray(value); +const DATA_KEYWORDS = new Set(["default", "examples", "enum", "const"]); +const SCHEMA_MAP_KEYWORDS = new Set(["properties", "patternProperties", "$defs"]); + +type Path = ReadonlyArray; + +const visitSchemas = ( + node: unknown, + visit: (node: Node, path: Path, siblings: ReadonlyArray | undefined) => void, + path: Path = [], + siblings?: ReadonlyArray, +): void => { + if (!isNode(node)) return; + visit(node, path, siblings); + for (const [keyword, value] of Object.entries(node)) { + if (DATA_KEYWORDS.has(keyword)) continue; + if (SCHEMA_MAP_KEYWORDS.has(keyword) && isNode(value)) { + for (const [name, child] of Object.entries(value)) { + visitSchemas(child, visit, [...path, keyword, name]); + } + } else if (Array.isArray(value)) { + value.forEach((child, index) => { + visitSchemas( + child, + visit, + [...path, keyword, index], + keyword === "anyOf" ? value : undefined, + ); + }); + } else { + visitSchemas(value, visit, [...path, keyword]); + } + } +}; + +const isEnvBranch = (node: unknown): boolean => + isNode(node) && + node.type === "string" && + typeof node.pattern === "string" && + Object.keys(node).length === 2; + +const collectEnvBranches = (document: unknown): Array => { + const found: Array = []; + visitSchemas(document, (node, _path, siblings) => { + if (siblings !== undefined && isEnvBranch(node)) found.push(node); + }); + return found; +}; + +const patternOf = (node: Node): string => { + if (typeof node.pattern !== "string") throw new Error("expected the node to carry a pattern"); + return node.pattern; +}; + +// $schema is dropped so the tests do not depend on which draft the header names. +const compile = (document: Node): ValidateFunction => { + const schema = Object.fromEntries(Object.entries(document).filter(([key]) => key !== "$schema")); + return new Ajv2020({ strict: false }).compile(schema); +}; + +const errorsFor = (validate: ValidateFunction, value: unknown): ReadonlyArray => + validate(value) ? [] : [...(validate.errors ?? [])]; + +const failingPaths = (validate: ValidateFunction, value: unknown): ReadonlyArray => [ + ...new Set(errorsFor(validate, value).map((error) => error.instancePath)), +]; + +const envPatternOf = (document: Node): string => + patternOf(at(document, "properties", "api", "properties", "max_rows", "anyOf", 1)); + +const RECORD_KEYS = ["sample", "SAMPLE"]; + +const collectLeaves = (node: unknown, path: Path = []): Array => { + if (!isNode(node)) return []; + const { properties, patternProperties, additionalProperties, anyOf, items } = node; + if (isNode(properties) || isNode(patternProperties) || isNode(additionalProperties)) { + return [ + ...Object.entries(isNode(properties) ? properties : {}).flatMap(([name, child]) => + collectLeaves(child, [...path, name]), + ), + ...Object.entries(isNode(patternProperties) ? patternProperties : {}).flatMap( + ([pattern, child]) => { + const key = RECORD_KEYS.find((candidate) => new RegExp(pattern, "u").test(candidate)); + return key === undefined ? [] : collectLeaves(child, [...path, key]); + }, + ), + ...collectLeaves(additionalProperties, [...path, "sample"]), + ]; + } + if (Array.isArray(anyOf) && anyOf.some((member) => isNode(member) && member.type === "object")) { + return anyOf.flatMap((member) => collectLeaves(member, path)); + } + if (isNode(items) && items.type === "object") return collectLeaves(items, [...path, 0]); + return [[path, node]]; +}; + +const envValueFor = (leaf: Node): string | undefined => { + const typed = Array.isArray(leaf.anyOf) ? at(leaf, "anyOf", 0) : leaf; + switch (typed.type) { + case "boolean": + return "false"; + case "number": + case "integer": + return "1"; + case "array": + return "a,b"; + case "string": + return Array.isArray(typed.enum) ? String(typed.enum.find((value) => value !== "")) : "x"; + default: + return undefined; + } +}; + +const documentAt = (path: Path, value: unknown): unknown => + path.reduceRight( + (child, key) => (typeof key === "number" ? [child] : { [key]: child }), + value, + ); + +const isAtOrBelow = (issuePath: ReadonlyArray, path: Path): boolean => + path.every((key, index) => String(issuePath[index]) === String(key)); + +const schemaAcceptsAt = (validate: ValidateFunction, path: Path, value: unknown): boolean => + !failingPaths(validate, documentAt(path, value)).some((pointer) => + isAtOrBelow(pointer.split("/").slice(1), path), + ); + +// Checks are off so only type acceptance is compared, not value rules such as port ranges. +const decodeCli = Schema.decodeUnknownResult(CliConfigSchema, { + errors: "all", + disableChecks: true, +}); +const toIssues = SchemaIssue.makeFormatterStandardSchemaV1(); + +const loaderAcceptsAt = ( + path: Path, + value: unknown, + env: Readonly>, +): boolean => { + const resolved = interpolateEnvReferencesAgainstSchema( + documentAt(path, value), + env, + CliConfigSchema, + { + goViperCompat: true, + }, + ); + const result = decodeCli(resolved); + if (Result.isSuccess(result)) return true; + return !toIssues(result.failure.issue).issues.some((issue) => + isAtOrBelow( + (issue.path ?? []).map((segment) => (typeof segment === "object" ? segment.key : segment)), + path, + ), + ); +}; + const collectSecretPaths = (document: unknown): Array => { const found = new Set(); const visit = (node: unknown, path: ReadonlyArray): void => { @@ -90,3 +253,398 @@ describe("published JSON Schema documents", () => { expect(JSON.stringify(toCliConfigJsonSchema())).not.toContain("x-env-deferred"); }); }); + +describe("env() references in the published CLI JSON Schema", () => { + const cli = toCliConfigJsonSchema(); + const validateCli = compile(cli); + + const baseline = { + project_id: "demo", + api: { port: 54321, max_rows: 1000 }, + auth: { enable_signup: true }, + db: { settings: { max_connections: 100 } }, + compute: { web: { instances: 2 } }, + storage: { file_size_limit: "50MiB" }, + remotes: { staging: { api: { max_rows: 500 }, compute: { web: { instances: 4 } } } }, + }; + + describe("validation", () => { + test("a document of plain literals validates", () => { + expect(errorsFor(validateCli, baseline)).toEqual([]); + }); + + test.each([ + ["api.max_rows", { api: { max_rows: "env(MAX_ROWS)" } }], + ["api.port", { api: { port: "env(API_PORT)" } }], + ["auth.enable_signup", { auth: { enable_signup: "env(ENABLE_SIGNUP)" } }], + ["db.settings.max_connections", { db: { settings: { max_connections: "env(MAX_CONN)" } } }], + ["compute..instances", { compute: { web: { instances: "env(WEB_INSTANCES)" } } }], + ["storage.file_size_limit", { storage: { file_size_limit: "env(FILE_SIZE_LIMIT)" } }], + ["db.pooler.pool_mode", { db: { pooler: { pool_mode: "env(POOL_MODE)" } } }], + ["realtime.ip_version", { realtime: { ip_version: "env(IP_VERSION)" } }], + [ + "auth.additional_redirect_urls", + { auth: { additional_redirect_urls: "env(REDIRECT_URLS)" } }, + ], + ["remotes..api.max_rows", { remotes: { staging: { api: { max_rows: "env(ROWS)" } } } }], + [ + "remotes..auth.enable_signup", + { remotes: { staging: { auth: { enable_signup: "env(SIGNUP)" } } } }, + ], + [ + "remotes..compute..instances", + { remotes: { staging: { compute: { web: { instances: "env(WEB)" } } } } }, + ], + ])("accepts env() on %s", (_field, document) => { + expect(errorsFor(validateCli, document)).toEqual([]); + }); + + test.each([ + ["a numeric string in a number field", { api: { max_rows: "1000" } }, "/api/max_rows"], + ["a word in a boolean field", { auth: { enable_signup: "yes" } }, "/auth/enable_signup"], + ["an unclosed env() reference", { api: { port: "env(PORT" } }, "/api/port"], + [ + "an env() reference with leading whitespace", + { auth: { enable_signup: " env(X)" } }, + "/auth/enable_signup", + ], + ["a boolean in a number field", { api: { max_rows: true } }, "/api/max_rows"], + [ + "an unknown value in an enum field", + { db: { pooler: { pool_mode: "statement" } } }, + "/db/pooler/pool_mode", + ], + [ + "a fractional count in an integer field", + { compute: { web: { instances: 1.5 } } }, + "/compute/web/instances", + ], + [ + "a negative count in a field with a minimum", + { compute: { web: { instances: -1 } } }, + "/compute/web/instances", + ], + [ + "a mistyped literal under a remote", + { remotes: { staging: { api: { port: "env(PORT" } } } }, + "/remotes/staging/api/port", + ], + [ + "a boolean in the string-or-number union", + { storage: { file_size_limit: true } }, + "/storage/file_size_limit", + ], + ])("rejects %s", (_scenario, document, path) => { + expect(validateCli(document)).toBe(false); + expect(failingPaths(validateCli, document)).toContain(path); + }); + }); + + describe("published structure", () => { + test("keeps hover docs on the outer node and the type in the first branch", () => { + const maxRows = at(cli, "properties", "api", "properties", "max_rows"); + expect(maxRows.description).toMatch(/maximum number of rows/); + expect(maxRows.default).toBe(1000); + expect(maxRows).not.toHaveProperty("type"); + expect(at(maxRows, "anyOf", 0)).toEqual({ type: "number" }); + }); + + test("keeps examples on the outer node and minimum in the typed branch of an integer leaf", () => { + const patterned = at(cli, "properties", "compute", "anyOf", 0, "patternProperties"); + const entries = Object.values(patterned); + expect(entries).toHaveLength(1); + const instances = at(entries[0], "properties", "instances"); + expect(instances.description).toMatch(/Number of instances/); + expect(instances.examples).toEqual([3]); + expect(instances).not.toHaveProperty("minimum"); + expect(at(instances, "anyOf", 0)).toEqual({ type: "integer", minimum: 0 }); + }); + + test("keeps hover docs on the outer node and the enum in the typed branch of a string enum", () => { + const poolMode = at( + cli, + "properties", + "db", + "properties", + "pooler", + "properties", + "pool_mode", + ); + expect(poolMode.description).toMatch(/server connection can be reused/); + expect(poolMode.default).toBe("transaction"); + expect(at(poolMode, "anyOf", 0)).toEqual({ + type: "string", + enum: ["transaction", "session"], + }); + expect(at(poolMode, "anyOf", 1)).toEqual({ type: "string", pattern: envPatternOf(cli) }); + }); + + test("appends a single env branch to a union without wrapping its number member", () => { + const fileSizeLimit = at(cli, "properties", "storage", "properties", "file_size_limit"); + expect(fileSizeLimit.anyOf).toHaveLength(3); + expect(at(fileSizeLimit, "anyOf", 0)).toEqual({ type: "string" }); + expect(at(fileSizeLimit, "anyOf", 1)).toEqual({ type: "number" }); + expect(at(fileSizeLimit, "anyOf", 2)).toEqual({ type: "string", pattern: envPatternOf(cli) }); + }); + }); + + describe("agreement with the loader's env() matching", () => { + const samples = [ + "env(MAX_ROWS)", + "env(max_rows)", + "env(_X1)", + "env(1ST)", + "env()", + "env(A B)", + "env(A-B)", + "env(日本語)", + "env((X))", + "env(env(X))", + "env(X))", + "ENV(X)", + "Env(X)", + "env(X", + "env X)", + " env(X)", + "env(X) ", + "prefix env(X)", + "env(X)suffix", + "env(A\nB)", + "env(X)\n", + "env", + "$(X)", + "${X}", + "1000", + "true", + "", + ]; + const validatePort = (value: unknown) => validateCli({ api: { port: value } }); + + test("a numeric field accepts exactly the strings the loader treats as references", () => { + expect(samples.filter((s) => validatePort(s) !== isEnvReference(s, true))).toEqual([]); + }); + + test("strict-mode references are a subset of what the schema accepts", () => { + expect(samples.filter((s) => isEnvReference(s, false) && !validatePort(s))).toEqual([]); + }); + + test("every env branch in the document carries the same pattern", () => { + const branches = collectEnvBranches(cli); + expect(branches.length).toBeGreaterThan(0); + expect(new Set(branches.map(patternOf))).toEqual(new Set([envPatternOf(cli)])); + }); + + test("every field accepts env() in the schema exactly when the loader resolves it", () => { + const port = ["api", "port"]; + expect(loaderAcceptsAt(port, "env(SAMPLE)", {})).toBe(false); + expect(loaderAcceptsAt(port, "env(SAMPLE)", { SAMPLE: "1" })).toBe(true); + + const leaves = collectLeaves(cli).flatMap(([path, leaf]) => { + const value = envValueFor(leaf); + return value === undefined ? [] : [{ path, value }]; + }); + const mismatches: Array = []; + for (const { path, value } of leaves) { + const schema = schemaAcceptsAt(validateCli, path, "env(SAMPLE)"); + const loader = loaderAcceptsAt(path, "env(SAMPLE)", { SAMPLE: value }); + if (schema !== loader) + mismatches.push(`${path.join(".")}: schema ${schema}, loader ${loader}`); + } + expect(leaves.length).toBeGreaterThan(500); + expect(mismatches).toEqual([]); + }, 60_000); + }); +}); + +describe("toPublishedJsonSchemaDocument acceptEnvReferences", () => { + const publish = (schema: Schema.Constraint, acceptEnvReferences?: boolean) => + toPublishedJsonSchemaDocument(schema, { + onExcessProperty: "error", + ...(acceptEnvReferences === undefined ? {} : { acceptEnvReferences }), + }); + const envBranch = { type: "string", pattern: ENV_PATTERN }; + + test("renders no env branch unless the option is on", () => { + const schema = Schema.Struct({ + count: Schema.Finite, + flag: Schema.Boolean, + whole: Schema.Int, + list: Schema.Array(Schema.Finite), + maybe: Schema.Union([Schema.Finite, Schema.Null]), + }); + const off = publish(schema); + expect(publish(schema, false)).toEqual(off); + expect(collectEnvBranches(off)).toEqual([]); + expect(JSON.stringify(off)).not.toContain('"pattern"'); + expect(collectEnvBranches(publish(schema, true))).toHaveLength(5); + }); + + test("wraps properties named after JSON Schema keywords without losing a key", () => { + const document = publish( + Schema.Struct({ type: Schema.Finite, default: Schema.Boolean, enum: Schema.Int }), + true, + ); + expect(Object.keys(at(document, "properties"))).toEqual(["type", "default", "enum"]); + expect(at(document, "required")).toEqual(["type", "default", "enum"]); + expect(at(document, "properties", "type").anyOf).toEqual([{ type: "number" }, envBranch]); + expect(at(document, "properties", "default").anyOf).toEqual([{ type: "boolean" }, envBranch]); + expect(at(document, "properties", "enum").anyOf).toEqual([{ type: "integer" }, envBranch]); + }); + + test("hoists title, description, default and examples out of the typed branch", () => { + const document = publish( + Schema.Struct({ + count: Schema.Finite.annotate({ + title: "Count", + description: "How many.", + default: 3, + examples: [4], + }), + }), + true, + ); + expect(at(document, "properties", "count")).toEqual({ + title: "Count", + description: "How many.", + default: 3, + examples: [4], + anyOf: [{ type: "number" }, envBranch], + }); + }); + + // The filter's toJsonSchema fragment is the one way to put object-valued const/enum into a document. + test("leaves default, examples, const and enum data alone even when it looks like a schema", () => { + const schema = Schema.Struct({ + annotated: Schema.Record(Schema.String, Schema.Unknown).annotate({ + default: { type: "number", nested: [{ type: "boolean" }] }, + examples: [{ type: "integer" }, { list: [{ type: "number" }] }], + }), + constrained: Schema.Record(Schema.String, Schema.Unknown).check( + Schema.makeFilter(() => true, { + toJsonSchema: () => ({ + const: { type: "number" }, + enum: [{ type: "boolean" }, { type: "number" }], + }), + }), + ), + }); + const off = publish(schema, false); + const on = publish(schema, true); + expect(at(on, "properties", "annotated").default).toEqual({ + type: "number", + nested: [{ type: "boolean" }], + }); + expect(at(on, "properties", "constrained", "allOf", 0).const).toEqual({ type: "number" }); + expect(on).toEqual(off); + expect(JSON.stringify(on)).toBe(JSON.stringify(off)); + }); + + test("wraps string enums but leaves plain and env() strings alone", () => { + const document = publish( + Schema.Struct({ + mode: Schema.Literals(["a", "b"]), + name: Schema.String, + token: env(), + }), + true, + ); + expect(at(document, "properties", "mode").anyOf).toEqual([ + { type: "string", enum: ["a", "b"] }, + envBranch, + ]); + expect(at(document, "properties", "name")).toEqual({ type: "string" }); + expect(at(document, "properties", "token")).toEqual(envBranch); + }); + + test("appends one env branch to a union and leaves its members unwrapped", () => { + const document = publish( + Schema.Struct({ + nullable: Schema.Union([Schema.Finite, Schema.Null]), + mixed: Schema.Union([Schema.Boolean, Schema.String]), + }), + true, + ); + expect(at(document, "properties", "nullable").anyOf).toEqual([ + { type: "number" }, + { type: "null" }, + envBranch, + ]); + expect(at(document, "properties", "mixed").anyOf).toEqual([ + { type: "boolean" }, + { type: "string" }, + envBranch, + ]); + }); + + test("does not add a second env branch to a union that already has one", () => { + const document = publish( + Schema.Struct({ + count: Schema.Union([Schema.Finite, env()]), + token: Schema.Union([Schema.Finite, env({ description: "Token", secret: true })]), + }), + true, + ); + expect(at(document, "properties", "count").anyOf).toEqual([{ type: "number" }, envBranch]); + const token = at(document, "properties", "token").anyOf as Array; + expect(token).toHaveLength(2); + expect(token[0]).toEqual({ type: "number" }); + }); + + test("wraps numbers and booleans inside arrays and record values", () => { + const document = publish( + Schema.Struct({ + counts: Schema.Array(Schema.Finite), + flags: Schema.Array(Schema.Boolean), + limits: Schema.Record(Schema.String, Schema.Finite), + }), + true, + ); + const validate = compile(document); + expect( + errorsFor(validate, { + counts: [1, "env(COUNT)"], + flags: [true, "env(FLAG)"], + limits: { a: 1, b: "env(LIMIT)" }, + }), + ).toEqual([]); + expect(failingPaths(validate, { counts: ["1"], flags: [], limits: {} })).toContain("/counts/0"); + expect(failingPaths(validate, { counts: [], flags: ["true"], limits: {} })).toContain( + "/flags/0", + ); + expect(failingPaths(validate, { counts: [], flags: [], limits: { a: "env(X" } })).toContain( + "/limits/a", + ); + }); + + test("accepts env() in place of a whole string list but not a list of enums", () => { + const document = publish( + Schema.Struct({ + names: Schema.Array(Schema.String), + modes: Schema.Array(Schema.Literals(["a", "b"])), + }), + true, + ); + const validate = compile(document); + expect(errorsFor(validate, { names: "env(NAMES)", modes: ["a", "env(MODE)"] })).toEqual([]); + expect(failingPaths(validate, { names: [], modes: "env(MODES)" })).toContain("/modes"); + }); + + test("wraps numbers and booleans in shared definitions", () => { + const Port = Schema.Finite.annotate({ identifier: "Port" }); + const Flag = Schema.Boolean.annotate({ identifier: "Flag" }); + const document = publish(Schema.Struct({ a: Port, b: Port, c: Flag }), true); + expect(at(document, "$defs", "Port").anyOf).toEqual([{ type: "number" }, envBranch]); + expect(at(document, "$defs", "Flag").anyOf).toEqual([{ type: "boolean" }, envBranch]); + const validate = compile(document); + expect(errorsFor(validate, { a: "env(A)", b: 2, c: "env(C)" })).toEqual([]); + expect(failingPaths(validate, { a: "x", b: 2, c: true })).toContain("/a"); + }); + + test("the project config schema carries no env-reference branch", () => { + const project = toProjectConfigJsonSchema(); + const maxRows = at(project, "properties", "api", "properties", "max_rows"); + expect(maxRows.type).toBe("number"); + expect(maxRows).not.toHaveProperty("anyOf"); + expect(collectEnvBranches(project)).toEqual([]); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8673924f0..e0be1578fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,6 +584,9 @@ importers: '@vitest/coverage-v8': specifier: 'catalog:' version: 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@5.0.0) + ajv: + specifier: ^8.20.0 + version: 8.20.0 effect: specifier: 'catalog:' version: 4.0.2 From 495820106f3c7ad27f8d49841e0cf8a7c310e866 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:18:07 +0000 Subject: [PATCH 31/33] feat(cli): fall back to ecr public for slim images (CLI-2874) (#7350) ## TL;DR adds an ECR Public fallback for slim images, so a GHCR rate limit no longer fails `supabase start` ## whats introduced? with `SUPABASE_USE_SLIM_IMAGES` on, slim images only came from GHCR, so a `toomanyrequests` burst failed the command a failed GHCR pull now falls back to the same tag and digest on `public.ecr.aws/supabase/cli`, the mirror the stack already uses ps: containers started from the mirror keep their slim healthchecks and pass the `db` stale image guard
visual rep: ```mermaid flowchart LR pull["slim image pull"] --> ghcr["ghcr.io/supabase/cli"] ghcr -->|ok| run["container starts"] ghcr -->|"fails 3 times"| ecr["public.ecr.aws/supabase/cli
same tag and digest"] ecr --> run ```
## ref - closes: https://github.com/supabase/cli/issues/7338 --- .../db-bootstrap/slim-runtime.ts | 2 +- .../docker-image-resolve.unit.test.ts | 30 ++++++++ .../src/command-internal/docker-registry.ts | 7 +- .../docker-registry.unit.test.ts | 10 +-- apps/cli/src/commands/db/dump/SIDE_EFFECTS.md | 26 +++---- apps/cli/src/commands/db/pull/SIDE_EFFECTS.md | 32 ++++----- .../cli/src/commands/db/reset/SIDE_EFFECTS.md | 38 +++++----- .../declarative/generate/SIDE_EFFECTS.md | 22 +++--- .../schema/declarative/sync/SIDE_EFFECTS.md | 18 ++--- .../shared/pgdelta.seam.integration.test.ts | 56 +++++++++------ .../commands/db/shared/pgdelta.seam.layer.ts | 2 +- .../cli/src/commands/db/start/SIDE_EFFECTS.md | 41 +++++------ .../commands/functions/deploy/SIDE_EFFECTS.md | 24 +++---- .../functions/download/SIDE_EFFECTS.md | 2 +- .../commands/functions/serve/SIDE_EFFECTS.md | 24 +++---- .../commands/migration/squash/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/start/SIDE_EFFECTS.md | 29 ++++---- .../start/start.slim-images.e2e.test.ts | 29 ++++++-- apps/cli/src/shared/services/slim-images.ts | 17 +++-- .../shared/services/slim-images.unit.test.ts | 69 ++++++++++--------- packages/stack/src/Artifacts.ts | 3 +- packages/stack/src/internal/artifacts.ts | 3 + 22 files changed, 283 insertions(+), 203 deletions(-) diff --git a/apps/cli/src/command-internal/db-bootstrap/slim-runtime.ts b/apps/cli/src/command-internal/db-bootstrap/slim-runtime.ts index bc65badb39..c9e65587eb 100644 --- a/apps/cli/src/command-internal/db-bootstrap/slim-runtime.ts +++ b/apps/cli/src/command-internal/db-bootstrap/slim-runtime.ts @@ -1,7 +1,7 @@ /** * Slim-image runtime contracts that still differ from docker.io. Spec builders * switch on {@link usesSlimImageRuntime} so flag-off stays byte-identical even - * if a caller passes a ghcr-shaped override. + * if a caller passes a slim-shaped override. * * Slim auth, storage, Vector, and the Elixir images (realtime, analytics, * pooler) ship BusyBox wget — not GNU wget and not curl. BusyBox documents diff --git a/apps/cli/src/command-internal/docker-image-resolve.unit.test.ts b/apps/cli/src/command-internal/docker-image-resolve.unit.test.ts index 10eefc1256..3615514ab6 100644 --- a/apps/cli/src/command-internal/docker-image-resolve.unit.test.ts +++ b/apps/cli/src/command-internal/docker-image-resolve.unit.test.ts @@ -195,6 +195,36 @@ describe("makeDockerImageResolver", () => { }), ); + it.effect("pulls a slim image from ECR Public once GHCR keeps rate-limiting it", () => + Effect.gen(function* () { + const originalWrite = globalThis.process.stderr.write.bind(globalThis.process.stderr); + globalThis.process.stderr.write = (() => true) as typeof globalThis.process.stderr.write; + + try { + const digest = "sha256:8fb7ae7cd0d8121c460c8756d7f2791ce9a19c3331b46987456dfa4f8b45d701"; + const slim = `ghcr.io/supabase/cli/postgres:17.11.0.004-r1@${digest}`; + const mirror = `public.ecr.aws/supabase/cli/postgres:17.11.0.004-r1@${digest}`; + const throttled = { + exitCode: 1, + stderr: "Error response from daemon: toomanyrequests: retry-after: 173.687µs", + }; + const mock = mockSpawner([throttled, throttled, throttled, { exitCode: 0 }]); + const fiber = yield* makeDockerImageResolver(mock.spawner)(slim).pipe( + Effect.forkChild({ startImmediately: true }), + ); + + yield* TestClock.adjust("4 seconds"); + yield* TestClock.adjust("8 seconds"); + const image = yield* Fiber.join(fiber); + + expect(mock.pulls).toEqual([slim, slim, slim, mirror]); + expect(image).toBe(mirror); + } finally { + globalThis.process.stderr.write = originalWrite; + } + }), + ); + it.effect( "does not inject a blank line before the banner when the child error is newline-terminated", () => diff --git a/apps/cli/src/command-internal/docker-registry.ts b/apps/cli/src/command-internal/docker-registry.ts index 186b825de3..47df502b4f 100644 --- a/apps/cli/src/command-internal/docker-registry.ts +++ b/apps/cli/src/command-internal/docker-registry.ts @@ -3,9 +3,10 @@ * overrides the default ECR mirror; `docker.io` returns the image unchanged, and any other value * rewrites it to `/supabase/`. Callers that can retry pulls should * use {@link getRegistryImageUrlCandidates} instead, which falls back through GHCR and the - * source image. Slim images ({@link isSlimImageRef}) skip every rewrite — there's no mirror to - * redirect them to. + * source image. Slim images ({@link isSlimImageRef}) skip these rewrites; their only fallback is + * the ECR Public mirror at the same tag and digest. */ +import { slimImageMirrors } from "@supabase/stack/internal/artifacts"; import { Config, ConfigProvider, Effect, Option } from "effect"; import { isSlimImageRef } from "../shared/services/slim-images.ts"; @@ -67,7 +68,7 @@ export function getRegistryImageUrlCandidates( projectEnvValues?: Readonly>, ): Effect.Effect, Config.ConfigError> { if (isSlimImageRef(imageName)) { - return Effect.succeed([imageName]); + return Effect.succeed([imageName, ...slimImageMirrors(imageName)]); } return registryOverride(projectEnvValues).pipe( diff --git a/apps/cli/src/command-internal/docker-registry.unit.test.ts b/apps/cli/src/command-internal/docker-registry.unit.test.ts index a1b857206a..8489afdcbd 100644 --- a/apps/cli/src/command-internal/docker-registry.unit.test.ts +++ b/apps/cli/src/command-internal/docker-registry.unit.test.ts @@ -102,12 +102,14 @@ describe("getRegistryImageUrl", () => { ).toBe("merged.example/supabase/pg_prove:3.36"); }); - // Published only under ghcr.io/supabase/cli; rewriting by last path segment would misroute it. + // Published under ghcr.io/supabase/cli; rewriting by last path segment would misroute it. const SLIM_IMAGE = "ghcr.io/supabase/cli/postgres:17.6.1.165"; + const SLIM_IMAGE_MIRROR = "public.ecr.aws/supabase/cli/postgres:17.6.1.165"; it("leaves a slim image unrewritten, whatever the registry override says", () => { for (const registry of [undefined, "public.ecr.aws", "docker.io", "my.mirror.example"]) { expect(resolveImage(registry, SLIM_IMAGE)).toBe(SLIM_IMAGE); + expect(resolveImage(registry, SLIM_IMAGE_MIRROR)).toBe(SLIM_IMAGE_MIRROR); } expect( resolveImage(undefined, SLIM_IMAGE, { @@ -116,15 +118,15 @@ describe("getRegistryImageUrl", () => { ).toBe(SLIM_IMAGE); }); - it("plans a single pull candidate for a slim image", () => { + it("plans the slim image, then its ECR Public mirror, whatever the registry override says", () => { for (const registry of [undefined, "public.ecr.aws", "docker.io", "my.mirror.example"]) { - expect(resolveCandidates(registry, SLIM_IMAGE)).toEqual([SLIM_IMAGE]); + expect(resolveCandidates(registry, SLIM_IMAGE)).toEqual([SLIM_IMAGE, SLIM_IMAGE_MIRROR]); } expect( resolveCandidates(undefined, SLIM_IMAGE, { SUPABASE_INTERNAL_IMAGE_REGISTRY: "my.mirror.example", }), - ).toEqual([SLIM_IMAGE]); + ).toEqual([SLIM_IMAGE, SLIM_IMAGE_MIRROR]); }); it("still rewrites the non-slim ghcr.io/supabase namespace", () => { diff --git a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md index a35d069505..e63963126a 100644 --- a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md @@ -35,19 +35,19 @@ image), to stdout or `--file`. ## Environment Variables -| Variable | Purpose | -| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `SUPABASE_DB_PASSWORD` (`DB_PASSWORD` viper key; `--password`/`-p` overrides) | remote DB password | -| `SUPABASE_ACCESS_TOKEN` | `--linked` auth | -| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB `pg_dump` image | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | -| `BITBUCKET_CLONE_DIR` | clears the `label:disable` security option that mounted TLS files add | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | rewrite the pg image registry for compose dumps; stack dumps use the catalog image pin unchanged | -| `SUPABASE_USE_SLIM_IMAGES` | resolve the current Postgres pin from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | -| `DOCKER_HOST` | docker daemon endpoint | -| `MSYSTEM`, `TERM_PROGRAM` | suppress the piped-stdout non-ASCII warning in MSYS/mintty sessions | -| `PG*` libpq | linked-project TLS, timeout, service; see `db push`. An invalid value fails before `pg_dump` runs; a remote target also hands them to `pg_dump` (see Notes) | -| `SSL_CERT_FILE`, `SSL_CERT_DIR` | a native `pg_dump` on libpq 16+ keeps `sslrootcert=system` when either is set; libpq 15 uses only `SSL_CERT_FILE` | +| Variable | Purpose | +| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SUPABASE_DB_PASSWORD` (`DB_PASSWORD` viper key; `--password`/`-p` overrides) | remote DB password | +| `SUPABASE_ACCESS_TOKEN` | `--linked` auth | +| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB `pg_dump` image | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | +| `BITBUCKET_CLONE_DIR` | clears the `label:disable` security option that mounted TLS files add | +| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | rewrite the pg image registry for compose dumps; stack dumps use the catalog image pin unchanged | +| `SUPABASE_USE_SLIM_IMAGES` | resolve the current Postgres pin from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli` | +| `DOCKER_HOST` | docker daemon endpoint | +| `MSYSTEM`, `TERM_PROGRAM` | suppress the piped-stdout non-ASCII warning in MSYS/mintty sessions | +| `PG*` libpq | linked-project TLS, timeout, service; see `db push`. An invalid value fails before `pg_dump` runs; a remote target also hands them to `pg_dump` (see Notes) | +| `SSL_CERT_FILE`, `SSL_CERT_DIR` | a native `pg_dump` on libpq 16+ keeps `sslrootcert=system` when either is set; libpq 15 uses only `SSL_CERT_FILE` | ## Exit Codes diff --git a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md index 9155ce69a8..a49112ebbf 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -128,22 +128,22 @@ at all, so nothing is cached for it. ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth for the linked target | no | -| `SUPABASE_DB_PASSWORD` | remote DB password (overridden by `-p`) | no | -| `SUPABASE_DB_SHADOW_PORT` | shadow container's host port (`db.shadow_port`) — NOT `SUPABASE_DB_PORT`, which the shadow never reads | no | -| `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | shadow container-config overrides, same as `db start`/`db reset` | no | -| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB shadow and initial-migra `pg_dump` images | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | -| `SUPABASE_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | -| `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow and initial-migra `pg_dump` containers onto an existing Docker network | no | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the current-pin shadow Postgres, `pg_dump`, PG15+ realtime/storage/auth migrate-job images (migration-style cold shadow), and (for migra) the edge-runtime image from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, flag-off `15.8.1.085`, and `deno_version = 1` stay on docker.io | no | -| `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | -| `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline state is saved under a managed snapshot key and restored into the next run's fresh stack database (see Notes) | no | -| `SUPABASE_EXPERIMENTAL_PG_DELTA` | force pg-delta diff engine | no | -| `SUPABASE_EXPERIMENTAL` | selects the deprecated in-process structured-dump export (same as `--declarative`) when `--declarative` is not set | no | -| `PG*` libpq | linked-project TLS, timeout, service; see `db push`. The `pg_dump` seed gets them (see `db dump`); the migra container gets none of these `PG*` TLS settings (migra gets the CLI's own bundled CA outside its OOM fallback) | no | +| Variable | Purpose | Required? | +| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | +| `SUPABASE_ACCESS_TOKEN` | auth for the linked target | no | +| `SUPABASE_DB_PASSWORD` | remote DB password (overridden by `-p`) | no | +| `SUPABASE_DB_SHADOW_PORT` | shadow container's host port (`db.shadow_port`) — NOT `SUPABASE_DB_PORT`, which the shadow never reads | no | +| `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | shadow container-config overrides, same as `db start`/`db reset` | no | +| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB shadow and initial-migra `pg_dump` images | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| `SUPABASE_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | +| `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow and initial-migra `pg_dump` containers onto an existing Docker network | no | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the current-pin shadow Postgres, `pg_dump`, PG15+ realtime/storage/auth migrate-job images (migration-style cold shadow), and (for migra) the edge-runtime image from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, flag-off `15.8.1.085`, and `deno_version = 1` stay on docker.io; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli` | no | +| `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | +| `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline state is saved under a managed snapshot key and restored into the next run's fresh stack database (see Notes) | no | +| `SUPABASE_EXPERIMENTAL_PG_DELTA` | force pg-delta diff engine | no | +| `SUPABASE_EXPERIMENTAL` | selects the deprecated in-process structured-dump export (same as `--declarative`) when `--declarative` is not set | no | +| `PG*` libpq | linked-project TLS, timeout, service; see `db push`. The `pg_dump` seed gets them (see `db dump`); the migra container gets none of these `PG*` TLS settings (migra gets the CLI's own bundled CA outside its OOM fallback) | no | ## Exit Codes diff --git a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md index 4b83d29bfe..946453b2db 100644 --- a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md @@ -162,25 +162,25 @@ the whole reset** (not just "skip buckets"). ## Environment Variables -| Variable | Purpose | Required? | -| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -| `DOCKER_HOST` / `DOCKER_CONTEXT` / `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` / `DOCKER_API_VERSION` / `DOCKER_CONFIG` | local path: ambient shell environment only (project dotenv files deliberately never override Docker client keys) — resolves the daemon endpoint for the running probe (in-process) and steers the spawned `docker`/`podman` CLI itself | no | -| `SUPABASE_ACCESS_TOKEN` | auth token for the `--linked` resolver path | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_DB_PASSWORD` | password for the linked/remote connection | no | -| `SUPABASE_YES` | auto-confirm the reset prompt and the local path's bucket-seed overwrite/prune prompts (shell or project dotenv, same as `seed buckets`) | no (also `--yes`) | -| `SUPABASE_EXPERIMENTAL` | selects the schema-files apply branch on either target | no (also `--experimental`) | -| `SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED` | overrides `[experimental.pgdelta].enabled`; a truthy value flips the reset gate (`experimental && resolvedVersion === "" && !toml.pgDelta.enabled`) back to timestamped migrations even with `--experimental` set — switches between two different destructive code paths | no | -| `SUPABASE_DB_MIGRATIONS_SCHEMA_PATHS` | overrides `[db.migrations].schema_paths` (viper `AutomaticEnv`, beats the config-file value) for the schema-files apply branch — genuinely effective on both targets now | no (no dedicated flag — config-file-only otherwise) | -| `SUPABASE_PROJECT_ID` | overrides the local container id; ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the image registry used to resolve the local path's container images (project `.env` or shell) | no (project `.env` or shell) | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the local-reset Postgres image and the realtime/storage/auth migrate-job images from slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | no (ambient shell only) | -| `SUPABASE_DB_PORT` / `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | local-path container-recreate config overrides, same as `db start` | no | -| `SUPABASE_API_PORT` / `SUPABASE_API_EXTERNAL_URL` / `SUPABASE_API_TLS_*` / `SUPABASE_API_ENABLED` | legacy-backend bucket-seed step only: override the matching `[api]` fields for the Storage gateway URL/TLS, same as `seed buckets` (shell or project dotenv; #6452). Not consulted on the stack backend (see above) | no | -| `SUPABASE_AUTH_JWT_SECRET` / `SUPABASE_AUTH_SERVICE_ROLE_KEY` | local path: override `auth.jwt_secret` / `auth.service_role_key` (shell or project dotenv, `encrypted:` decrypted) — feeds the recreated Postgres container (jwt secret), the fresh-database setup jobs (both) and the legacy-backend bucket-seed Storage service-role key, same as `seed buckets`. Not consulted on the stack backend, which uses the stack's own service-role JWT | no | -| `SUPABASE_NETWORK_ID` (`--network-id`) | forces the recreated container/network onto an existing Docker network — legacy backend only | no | -| `SUPABASE_EXPERIMENTAL_STACK` | `1`/`0` selects the stack backend for the local path, overriding `experimental.stack` | no (→ `experimental.stack` → legacy) | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | -| `PG*` libpq | linked-project TLS, timeout, service; see `db push` | no | +| Variable | Purpose | Required? | +| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | +| `DOCKER_HOST` / `DOCKER_CONTEXT` / `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` / `DOCKER_API_VERSION` / `DOCKER_CONFIG` | local path: ambient shell environment only (project dotenv files deliberately never override Docker client keys) — resolves the daemon endpoint for the running probe (in-process) and steers the spawned `docker`/`podman` CLI itself | no | +| `SUPABASE_ACCESS_TOKEN` | auth token for the `--linked` resolver path | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_DB_PASSWORD` | password for the linked/remote connection | no | +| `SUPABASE_YES` | auto-confirm the reset prompt and the local path's bucket-seed overwrite/prune prompts (shell or project dotenv, same as `seed buckets`) | no (also `--yes`) | +| `SUPABASE_EXPERIMENTAL` | selects the schema-files apply branch on either target | no (also `--experimental`) | +| `SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED` | overrides `[experimental.pgdelta].enabled`; a truthy value flips the reset gate (`experimental && resolvedVersion === "" && !toml.pgDelta.enabled`) back to timestamped migrations even with `--experimental` set — switches between two different destructive code paths | no | +| `SUPABASE_DB_MIGRATIONS_SCHEMA_PATHS` | overrides `[db.migrations].schema_paths` (viper `AutomaticEnv`, beats the config-file value) for the schema-files apply branch — genuinely effective on both targets now | no (no dedicated flag — config-file-only otherwise) | +| `SUPABASE_PROJECT_ID` | overrides the local container id; ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | +| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the image registry used to resolve the local path's container images (project `.env` or shell) | no (project `.env` or shell) | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the local-reset Postgres image and the realtime/storage/auth migrate-job images from slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli` | no (ambient shell only) | +| `SUPABASE_DB_PORT` / `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | local-path container-recreate config overrides, same as `db start` | no | +| `SUPABASE_API_PORT` / `SUPABASE_API_EXTERNAL_URL` / `SUPABASE_API_TLS_*` / `SUPABASE_API_ENABLED` | legacy-backend bucket-seed step only: override the matching `[api]` fields for the Storage gateway URL/TLS, same as `seed buckets` (shell or project dotenv; #6452). Not consulted on the stack backend (see above) | no | +| `SUPABASE_AUTH_JWT_SECRET` / `SUPABASE_AUTH_SERVICE_ROLE_KEY` | local path: override `auth.jwt_secret` / `auth.service_role_key` (shell or project dotenv, `encrypted:` decrypted) — feeds the recreated Postgres container (jwt secret), the fresh-database setup jobs (both) and the legacy-backend bucket-seed Storage service-role key, same as `seed buckets`. Not consulted on the stack backend, which uses the stack's own service-role JWT | no | +| `SUPABASE_NETWORK_ID` (`--network-id`) | forces the recreated container/network onto an existing Docker network — legacy backend only | no | +| `SUPABASE_EXPERIMENTAL_STACK` | `1`/`0` selects the stack backend for the local path, overriding `experimental.stack` | no (→ `experimental.stack` → legacy) | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| `PG*` libpq | linked-project TLS, timeout, service; see `db push` | no | ## Connection loss during migration apply diff --git a/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md b/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md index 454898f45b..5087e45520 100644 --- a/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md @@ -42,17 +42,17 @@ formatting without disabling safe compaction. ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` | no | -| `DB_PASSWORD` | password for `--linked` / `--db-url` | no | -| `SUPABASE_HOME` | overrides the `~/.supabase` root (access token and other CLI state) | no | -| `PGDELTA_DEBUG` | bundled-engine debug artifacts | no | -| `SUPABASE_SERVICES_HOSTNAME` | local DB host for `--local` | no | -| `DOCKER_HOST` | tcp daemon host used as the local DB host fallback | no | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the expected local `db` image for the stale-container guard from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | -| `PG*` libpq | linked-project TLS, timeout, service; see `db push` | no | +| Variable | Purpose | Required? | +| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` | no | +| `DB_PASSWORD` | password for `--linked` / `--db-url` | no | +| `SUPABASE_HOME` | overrides the `~/.supabase` root (access token and other CLI state) | no | +| `PGDELTA_DEBUG` | bundled-engine debug artifacts | no | +| `SUPABASE_SERVICES_HOSTNAME` | local DB host for `--local` | no | +| `DOCKER_HOST` | tcp daemon host used as the local DB host fallback | no | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the expected local `db` image for the stale-container guard from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io; a container started from the `public.ecr.aws/supabase/cli` mirror counts as slim | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| `PG*` libpq | linked-project TLS, timeout, service; see `db push` | no | ## Exit Codes diff --git a/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md b/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md index 4b21f138f9..024dbccea6 100644 --- a/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/schema/declarative/sync/SIDE_EFFECTS.md @@ -55,15 +55,15 @@ disabling safe compaction. ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | -| `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline state is saved under a managed snapshot key and restored into the next run's fresh stack database (see Notes) | no | -| `PGDELTA_DEBUG` | bundled-engine debug artifacts | no | -| `SUPABASE_SERVICES_HOSTNAME` | local DB host for the bootstrap generate | no | -| `DOCKER_HOST` | tcp daemon host used as the local DB host fallback | no | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the current-pin shadow Postgres and PG15+ realtime/storage/auth migrate-job images from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | no | -| `PG*` libpq | no-files bootstrap only, when the target prompt picks **Linked project**: linked-project TLS, timeout, service; see `db push`. Sync itself has no `--linked` flag | no | +| Variable | Purpose | Required? | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | +| `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline state is saved under a managed snapshot key and restored into the next run's fresh stack database (see Notes) | no | +| `PGDELTA_DEBUG` | bundled-engine debug artifacts | no | +| `SUPABASE_SERVICES_HOSTNAME` | local DB host for the bootstrap generate | no | +| `DOCKER_HOST` | tcp daemon host used as the local DB host fallback | no | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the current-pin shadow Postgres and PG15+ realtime/storage/auth migrate-job images from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli`; a container started from that mirror counts as slim | no | +| `PG*` libpq | no-files bootstrap only, when the target prompt picks **Linked project**: linked-project TLS, timeout, service; see `db push`. Sync itself has no `--linked` flag | no | ## Exit Codes diff --git a/apps/cli/src/commands/db/shared/pgdelta.seam.integration.test.ts b/apps/cli/src/commands/db/shared/pgdelta.seam.integration.test.ts index c056b58738..b581e9df22 100644 --- a/apps/cli/src/commands/db/shared/pgdelta.seam.integration.test.ts +++ b/apps/cli/src/commands/db/shared/pgdelta.seam.integration.test.ts @@ -52,28 +52,31 @@ const pgTag = vi.hoisted(() => { return match[1]; }); -vi.mock("@supabase/stack/internal/artifacts", () => { - const digest = "d348483ad1141c54bfb4eaae801f5385fe1c2970fc106f95f531b5247092d52c"; - const nativePin = { archive: digest, manifest: digest }; - return { - catalogPins: () => [ - { - service: "database", - sourceService: "postgres", - pin: { - upstreamVersion: pgTag, - revision: 0, - image: `ghcr.io/supabase/cli/postgres:${pgTag}-r0@sha256:${digest}`, - natives: { - "darwin-arm64": nativePin, - "linux-amd64": nativePin, - "linux-arm64": nativePin, +vi.mock("@supabase/stack/internal/artifacts", (importOriginal) => + importOriginal().then((actual) => { + const digest = "d348483ad1141c54bfb4eaae801f5385fe1c2970fc106f95f531b5247092d52c"; + const nativePin = { archive: digest, manifest: digest }; + return { + ...actual, + catalogPins: () => [ + { + service: "database", + sourceService: "postgres", + pin: { + upstreamVersion: pgTag, + revision: 0, + image: `ghcr.io/supabase/cli/postgres:${pgTag}-r0@sha256:${digest}`, + natives: { + "darwin-arm64": nativePin, + "linux-amd64": nativePin, + "linux-arm64": nativePin, + }, }, }, - }, - ], - }; -}); + ], + }; + }), +); /** * Integration coverage for the fully-native `declarativeSeamLayer`: `generate`/`sync`'s own @@ -270,6 +273,19 @@ describe("declarativeSeamLayer.ensureLocalPostgresImageCurrent", () => { }).pipe(Effect.provide(layer)); }); + it.effect("passes when a slim container was pulled from the ECR Public mirror", () => { + vi.stubEnv("SUPABASE_USE_SLIM_IMAGES", "true"); + const dir = tmp.current; + const { layer } = setup(dir, { + dbInspectImage: `public.ecr.aws/supabase/cli/postgres:${pgTag}-r0@sha256:d348483ad1141c54bfb4eaae801f5385fe1c2970fc106f95f531b5247092d52c`, + }); + return Effect.gen(function* () { + const seam = yield* DeclarativeSeam; + const exit = yield* seam.ensureLocalPostgresImageCurrent.pipe(Effect.exit); + expect(Exit.isSuccess(exit)).toBe(true); + }).pipe(Effect.provide(layer)); + }); + it.effect("bails out when inspect succeeds but the image name is unparseable", () => { vi.stubEnv("SUPABASE_USE_SLIM_IMAGES", "true"); const dir = tmp.current; diff --git a/apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts b/apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts index 79ae4290ea..04b0f18d5a 100644 --- a/apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts +++ b/apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts @@ -247,7 +247,7 @@ export const declarativeSeamLayer = Layer.effect( if (actual.length === 0 || actualTag === undefined || expectedTag === undefined) { return; } - // Slim refs never go through a registry mirror, so a family mismatch + // A slim ref's ECR Public mirror is the same family; any other family mismatch // (e.g. a docker.io container satisfying a ghcr.io/supabase/cli // expectation) is stale even when the upstream versions happen to match. const familyMismatch = isSlimImageRef(expected) !== isSlimImageRef(actual); diff --git a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md index 10b9290306..7e017bd662 100644 --- a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md @@ -41,6 +41,7 @@ composition reuses too — see that command's `SIDE_EFFECTS.md`): 13/15's published slim PG15 pin, `15.14.1.167`) to `ghcr.io/supabase/cli/postgres`; a historical `.temp/postgres-version` pin, PG14, OrioleDB tags the catalog does not pin, and flag-off majors 13/15 (`15.8.1.085`) stay on docker.io. + A failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli/postgres`. The restore entrypoint is the same on both families. 6. Wait for the container to become healthy (`db.health_timeout`, default `2m`). A timeout fails the command UNLESS `--from-backup` is set, in which case it is swallowed (a large @@ -123,26 +124,26 @@ API request over the active context's local unix socket / named pipe: ## Environment Variables -| Variable | Purpose | Required? | -| -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_PROJECT_ID` | overrides the local container id | no | -| `SUPABASE_DB_PORT` | overrides `db.port` (the published host port) | no | -| `SUPABASE_DB_MAJOR_VERSION` | overrides `db.major_version` (image selection, schema branch) | no | -| `SUPABASE_DB_HEALTH_TIMEOUT` | overrides `db.health_timeout` | no | -| `SUPABASE_DB_SETTINGS_*` | overrides individual `[db.settings]` fields | no | -| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version` (image + env) | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | -| `SUPABASE_EXPERIMENTAL_S3_{HOST,REGION,ACCESS_KEY,SECRET_KEY}` | OrioleDB S3 env overrides | no | -| `SUPABASE_REALTIME_ENABLED` | gates the fresh-volume realtime migrate job | no | -| `SUPABASE_REALTIME_IP_VERSION` / `_MAX_HEADER_LENGTH` | realtime migrate job env overrides | no | -| `SUPABASE_STORAGE_ENABLED` | gates the fresh-volume storage migrate job | no | -| `SUPABASE_STORAGE_FILE_SIZE_LIMIT` | storage migrate job env override | no | -| `SUPABASE_AUTH_ENABLED` | gates the fresh-volume auth migrate job | no | -| `SUPABASE_AUTH_EXTERNAL_URL` / `SUPABASE_AUTH_SITE_URL` | auth migrate job env overrides | no | -| `SUPABASE_AUTH_JWT_EXPIRY` | Postgres's `JWT_EXP` env / signing | no | -| `SUPABASE_EXPERIMENTAL` (or `--experimental`) | fresh volume + no pg-delta: applies `db.migrations.schema_paths` files instead of `migrations/*.sql` | no | -| `DOCKER_HOST` / `DOCKER_CONTEXT` / `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` / `DOCKER_API_VERSION` / `DOCKER_CONFIG` | Read from the ambient shell environment to pick the Docker daemon this whole command talks to (project dotenv files deliberately never override Docker client keys) | no | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the current Dockerfile pin (and majors 13/15's published slim PG15 pin, `15.14.1.167`) and PG15+ realtime/storage/auth migrate-job images from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); historical `.temp` pins, PG14, OrioleDB tags the catalog does not pin, and flag-off majors 13/15 (`15.8.1.085`) stay on docker.io | no | +| Variable | Purpose | Required? | +| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | +| `SUPABASE_PROJECT_ID` | overrides the local container id | no | +| `SUPABASE_DB_PORT` | overrides `db.port` (the published host port) | no | +| `SUPABASE_DB_MAJOR_VERSION` | overrides `db.major_version` (image selection, schema branch) | no | +| `SUPABASE_DB_HEALTH_TIMEOUT` | overrides `db.health_timeout` | no | +| `SUPABASE_DB_SETTINGS_*` | overrides individual `[db.settings]` fields | no | +| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version` (image + env) | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| `SUPABASE_EXPERIMENTAL_S3_{HOST,REGION,ACCESS_KEY,SECRET_KEY}` | OrioleDB S3 env overrides | no | +| `SUPABASE_REALTIME_ENABLED` | gates the fresh-volume realtime migrate job | no | +| `SUPABASE_REALTIME_IP_VERSION` / `_MAX_HEADER_LENGTH` | realtime migrate job env overrides | no | +| `SUPABASE_STORAGE_ENABLED` | gates the fresh-volume storage migrate job | no | +| `SUPABASE_STORAGE_FILE_SIZE_LIMIT` | storage migrate job env override | no | +| `SUPABASE_AUTH_ENABLED` | gates the fresh-volume auth migrate job | no | +| `SUPABASE_AUTH_EXTERNAL_URL` / `SUPABASE_AUTH_SITE_URL` | auth migrate job env overrides | no | +| `SUPABASE_AUTH_JWT_EXPIRY` | Postgres's `JWT_EXP` env / signing | no | +| `SUPABASE_EXPERIMENTAL` (or `--experimental`) | fresh volume + no pg-delta: applies `db.migrations.schema_paths` files instead of `migrations/*.sql` | no | +| `DOCKER_HOST` / `DOCKER_CONTEXT` / `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` / `DOCKER_API_VERSION` / `DOCKER_CONFIG` | Read from the ambient shell environment to pick the Docker daemon this whole command talks to (project dotenv files deliberately never override Docker client keys) | no | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the current Dockerfile pin (and majors 13/15's published slim PG15 pin, `15.14.1.167`) and PG15+ realtime/storage/auth migrate-job images from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); historical `.temp` pins, PG14, OrioleDB tags the catalog does not pin, and flag-off majors 13/15 (`15.8.1.085`) stay on docker.io; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli` | no | `--network-id` (a global CLI flag, not an environment variable — `command-internal/global-flags.ts`) forces every created container/network onto that Docker network instead of the generated diff --git a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md index d26ceb6bb7..7ef3b49ae0 100644 --- a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md @@ -50,18 +50,18 @@ Docker bundling may pull or run the configured edge-runtime image and uses the ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_PROJECT_ID` | optional project ref fallback; also read from project dotenv now (previously ambient-shell-only) | no | -| `SUPABASE_ENV` | selects environment-specific dotenv files (`.env..local`, `.env.`) | no (defaults to `development`) | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | selects the Functions bundler image registry; read from the ambient shell **or** project dotenv; unset resolves ECR->GHCR->Docker-Hub candidates in order instead of a single URL | no | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the Functions bundler image from the slim `ghcr.io/supabase/cli/edge-runtime` build (`true`/`1` enable); `deno_version = 1` and historical `.temp/edge-runtime-version` pins stay on docker.io; ambient shell only, unlike the neighboring registry override | no | -| `SUPABASE_NETWORK_ID` | overrides the generated `supabase_network_` Docker network name when `--network-id` isn't passed; read from the ambient shell or project dotenv | no | -| `BITBUCKET_CLONE_DIR` | when defined (including an empty value), skips creating the named Deno-cache volume and omits its bind mount from the bundler `docker run` (Bitbucket's restricted Docker environment rejects both); project values are passed explicitly to Docker setup | no | -| `SUPABASE_EDGE_RUNTIME_DENO_VERSION` | overrides `edge_runtime.deno_version` (which bundler image tag to use) when set, from the ambient shell or project dotenv — takes effect even with no `config.toml` on disk | no | -| `NPM_CONFIG_REGISTRY` | forwarded into Docker bundling when set (the only npm variable forwarded; `NPM_AUTH_TOKEN` is not) | no | -| `DEBUG` | enables verbose Docker bundle output when `true` | no | +| Variable | Purpose | Required? | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_PROJECT_ID` | optional project ref fallback; also read from project dotenv now (previously ambient-shell-only) | no | +| `SUPABASE_ENV` | selects environment-specific dotenv files (`.env..local`, `.env.`) | no (defaults to `development`) | +| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | selects the Functions bundler image registry; read from the ambient shell **or** project dotenv; unset resolves ECR->GHCR->Docker-Hub candidates in order instead of a single URL | no | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the Functions bundler image from the slim `ghcr.io/supabase/cli/edge-runtime` build (`true`/`1` enable); `deno_version = 1` and historical `.temp/edge-runtime-version` pins stay on docker.io; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli`; ambient shell only, unlike the neighboring registry override | no | +| `SUPABASE_NETWORK_ID` | overrides the generated `supabase_network_` Docker network name when `--network-id` isn't passed; read from the ambient shell or project dotenv | no | +| `BITBUCKET_CLONE_DIR` | when defined (including an empty value), skips creating the named Deno-cache volume and omits its bind mount from the bundler `docker run` (Bitbucket's restricted Docker environment rejects both); project values are passed explicitly to Docker setup | no | +| `SUPABASE_EDGE_RUNTIME_DENO_VERSION` | overrides `edge_runtime.deno_version` (which bundler image tag to use) when set, from the ambient shell or project dotenv — takes effect even with no `config.toml` on disk | no | +| `NPM_CONFIG_REGISTRY` | forwarded into Docker bundling when set (the only npm variable forwarded; `NPM_AUTH_TOKEN` is not) | no | +| `DEBUG` | enables verbose Docker bundle output when `true` | no | ## Exit Codes diff --git a/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md index 028e79b7b5..33906f61d0 100644 --- a/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md @@ -67,7 +67,7 @@ to stderr in machine-output modes (CLI-1546). | `SUPABASE_ENV` | Docker-unbundle path: selects environment-specific dotenv files (`.env..local`, `.env.`) | no (defaults to `development`) | | `BITBUCKET_CLONE_DIR` | Docker-unbundle path: when defined (including an empty value), skips creating the named Deno-cache volume and omits its bind mount from the `docker run` command (Bitbucket's restricted Docker environment rejects both) | no | | `SUPABASE_INTERNAL_IMAGE_REGISTRY` | selects the registry the edge-runtime unbundle image is pulled from (`getRegistryImageUrl`); read from the ambient shell **or** project dotenv (Docker-unbundle path); unset resolves ECR->GHCR->Docker-Hub candidates in order instead of a single URL — also consumed on the `--use-api` invocation even though it never pulls an image | no (defaults to `public.ecr.aws`) | -| `SUPABASE_USE_SLIM_IMAGES` | Docker-unbundle path: resolves the edge-runtime unbundle image from the slim `ghcr.io/supabase/cli/edge-runtime` build (`true`/`1` enable); `deno_version = 1` and historical `.temp/edge-runtime-version` pins stay on docker.io | no | +| `SUPABASE_USE_SLIM_IMAGES` | Docker-unbundle path: resolves the edge-runtime unbundle image from the slim `ghcr.io/supabase/cli/edge-runtime` build (`true`/`1` enable); `deno_version = 1` and historical `.temp/edge-runtime-version` pins stay on docker.io; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli` | no | | `SUPABASE_NETWORK_ID` | Docker-unbundle path: overrides the generated `supabase_network_` Docker network name when `--network-id` isn't passed; read from the ambient shell or project dotenv | no | | `SUPABASE_EDGE_RUNTIME_DENO_VERSION` | Docker-unbundle path: overrides `edge_runtime.deno_version` (which image tag to pull) when set, from the ambient shell or project dotenv — takes effect even with no `config.toml` on disk | no | diff --git a/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md index 02270298a4..3161b7d89d 100644 --- a/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md @@ -46,18 +46,18 @@ back to local keys. No scheme/host validation is performed on the discovered URL ## Environment Variables -| Variable | Purpose | Required? | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| `SUPABASE_PROFILE` | resolves the profile / API base URL | no (defaults to `supabase`) | -| `SUPABASE_WORKDIR` | overrides the project workdir | no (falls back to CLI cwd discovery) | -| `SUPABASE_PROJECT_ID` | config-service override for project identity | no | -| `SUPABASE_ENV` | selects environment-specific dotenv files (`.env..local`, `.env.`) | no (defaults to `development`) | -| env vars referenced by `supabase/config.toml` | config interpolation; the full ambient `process.env` is layered under the project `.env*` files and passed to config loading | no | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the edge-runtime Docker registry mirror; read from the ambient shell **or** project dotenv; unset resolves ECR->GHCR->Docker-Hub candidates in order instead of a single URL | no (defaults to `public.ecr.aws`) | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the edge-runtime image from the slim `ghcr.io/supabase/cli/edge-runtime` build (`true`/`1` enable); `deno_version = 1` and historical `.temp/edge-runtime-version` pins stay on docker.io | no | -| `SUPABASE_NETWORK_ID` | overrides the generated `supabase_network_` Docker network name when `--network-id` isn't passed; read from the ambient shell or project dotenv | no | -| `SUPABASE_EDGE_RUNTIME_DENO_VERSION` | overrides `edge_runtime.deno_version` (which image tag to pull) when set, from the ambient shell or project dotenv — takes effect even with no `config.toml` on disk | no | -| `BITBUCKET_CLONE_DIR` | when defined (including an empty value), skips creating the named Deno-cache volume and omits its bind mount from the edge-runtime `docker create` (Bitbucket's restricted Docker environment rejects both); project values are passed explicitly to Docker setup | no | +| Variable | Purpose | Required? | +| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `SUPABASE_PROFILE` | resolves the profile / API base URL | no (defaults to `supabase`) | +| `SUPABASE_WORKDIR` | overrides the project workdir | no (falls back to CLI cwd discovery) | +| `SUPABASE_PROJECT_ID` | config-service override for project identity | no | +| `SUPABASE_ENV` | selects environment-specific dotenv files (`.env..local`, `.env.`) | no (defaults to `development`) | +| env vars referenced by `supabase/config.toml` | config interpolation; the full ambient `process.env` is layered under the project `.env*` files and passed to config loading | no | +| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the edge-runtime Docker registry mirror; read from the ambient shell **or** project dotenv; unset resolves ECR->GHCR->Docker-Hub candidates in order instead of a single URL | no (defaults to `public.ecr.aws`) | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the edge-runtime image from the slim `ghcr.io/supabase/cli/edge-runtime` build (`true`/`1` enable); `deno_version = 1` and historical `.temp/edge-runtime-version` pins stay on docker.io; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli` | no | +| `SUPABASE_NETWORK_ID` | overrides the generated `supabase_network_` Docker network name when `--network-id` isn't passed; read from the ambient shell or project dotenv | no | +| `SUPABASE_EDGE_RUNTIME_DENO_VERSION` | overrides `edge_runtime.deno_version` (which image tag to pull) when set, from the ambient shell or project dotenv — takes effect even with no `config.toml` on disk | no | +| `BITBUCKET_CLONE_DIR` | when defined (including an empty value), skips creating the named Deno-cache volume and omits its bind mount from the edge-runtime `docker create` (Bitbucket's restricted Docker environment rejects both); project values are passed explicitly to Docker setup | no | ## Exit Codes diff --git a/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md b/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md index f5ac95e80b..a5bb10c8cc 100644 --- a/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md @@ -88,7 +88,7 @@ live shadow available. `SUPABASE_YES`, `DB_PASSWORD`, `SUPABASE_ACCESS_TOKEN`, `SUPABASE_SERVICES_HOSTNAME`, `DOCKER_HOST`/`DOCKER_CONTEXT`/`DOCKER_CONFIG`, `SUPABASE_NETWORK_ID`, -`SUPABASE_INTERNAL_IMAGE_REGISTRY`, `SUPABASE_USE_SLIM_IMAGES` (current-pin shadow Postgres and PG15+ realtime/storage/auth migrate-job images → slim `ghcr.io/supabase/cli`; historical pins, PG14, OrioleDB tags the catalog does not pin, flag-off `15.8.1.085` stay on docker.io), `SUPABASE_PROJECT_ID`, `SUPABASE_DEBUG`, +`SUPABASE_INTERNAL_IMAGE_REGISTRY`, `SUPABASE_USE_SLIM_IMAGES` (current-pin shadow Postgres and PG15+ realtime/storage/auth migrate-job images → slim `ghcr.io/supabase/cli`; a failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli`; historical pins, PG14, OrioleDB tags the catalog does not pin, flag-off `15.8.1.085` stay on docker.io), `SUPABASE_PROJECT_ID`, `SUPABASE_DEBUG`, `SUPABASE_EXPERIMENTAL`, `SUPABASE_SHADOW_CACHE` (stack shadow baseline cache; on by default, falsy disables restore and publication), `PG*` libpq on `--linked` (TLS, timeout, service; see `db push`). ## Exit Codes diff --git a/apps/cli/src/commands/start/SIDE_EFFECTS.md b/apps/cli/src/commands/start/SIDE_EFFECTS.md index 643804de2b..76860f2153 100644 --- a/apps/cli/src/commands/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/start/SIDE_EFFECTS.md @@ -127,7 +127,8 @@ Kong's `custom_nginx.template`, Vector's `vector.yaml`, and Postgres's own boots script (`postgresql.conf`-equivalent setup) are all rendered in memory and injected directly into each container's entrypoint (a `sh -c '... heredoc ...'` command) — never written to the host filesystem, since none of them carries secret content. -`SUPABASE_USE_SLIM_IMAGES` rewrites image names to `ghcr.io/supabase/cli/*`. +`SUPABASE_USE_SLIM_IMAGES` rewrites image names to `ghcr.io/supabase/cli/*`; a failed GHCR pull +falls back to the same tag and digest on `public.ecr.aws/supabase/cli/*`. Slim postgres, studio, pg-meta, and edge-runtime keep the docker.io health probes (`pg_isready` / `node`). Slim analytics keeps the docker.io start spec (`./logflare` migrate/start wrapper and BigQuery bind at @@ -194,19 +195,19 @@ not implemented. ## Environment Variables -| Variable | Purpose | Required? | -| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_*` (any dotted config field) | Generic Viper-style `AutomaticEnv` override of any `config.toml` field (e.g. `SUPABASE_AUTH_ENABLED`, `SUPABASE_API_PORT`) | no | -| `SUPABASE_EXPERIMENTAL` (or `--experimental`) | Fresh volume + no pg-delta: applies `db.migrations.schema_paths` files instead of `migrations/*.sql` (see "Fresh-volume DB setup" above) | no | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | Overrides the image registry used to resolve every service's image | no | -| `SUPABASE_PROJECT_ID` | Overrides the resolved local project id (env → config.toml → workdir basename) | no | -| `SUPABASE_WORKDIR` | Resolves `CommandSettings.workdir` | no | -| `SUPABASE_YES` (or `--yes`) | Auto-confirms the fresh-volume bucket-seed overwrite/prune prompts (shell or project dotenv, same as `seed buckets`) | no | -| `BITBUCKET_CLONE_DIR` | When non-empty, drops named volumes and `--security-opt` from every container create | no | -| `DOCKER_HOST` / `DOCKER_CONTEXT` / `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` / `DOCKER_API_VERSION` / `DOCKER_CONFIG` | Read (ambient shell OR a project `.env`/`.env.`/`.env.local` file) to discover the Docker daemon this whole command talks to; `DOCKER_HOST` is also re-derived and set on Vector's container env so it can reach the host's Docker socket for log collection | no | -| `KONG_NGINX_WORKER_PROCESSES` | Read (ambient shell or project dotenv) into Kong's own container env (defaults to `"1"` when unset) | no | -| `HTTP_PROXY` / `http_proxy` / `HTTPS_PROXY` / `https_proxy` / `NO_PROXY` / `no_proxy` | Bun proxy settings. After project dotenv and container creation, `start` appends `localhost,127.0.0.1,[::1]` to the effective no-proxy value before local Kong probes and seeding; it never changes project/container env and ends with this CLI process. | no | -| `SUPABASE_USE_SLIM_IMAGES` | Ambient `process.env` only (`true`/`1` enable) — not project dotenv. Rewrites current Dockerfile pins to `ghcr.io/supabase/cli/` (including PG15+ realtime/storage/auth migrate jobs). Kong, PG14, OrioleDB tags the catalog does not pin, historical Postgres pins, and `deno_version = 1` remain non-slim (still subject to `SUPABASE_INTERNAL_IMAGE_REGISTRY`). Majors 13/15 use the published slim PG15 pin (`15.14.1.167`) when the flag is on; flag-off keeps `15.8.1.085`. `SUPABASE_INTERNAL_IMAGE_REGISTRY` does not apply to slim refs | no | +| Variable | Purpose | Required? | +| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_*` (any dotted config field) | Generic Viper-style `AutomaticEnv` override of any `config.toml` field (e.g. `SUPABASE_AUTH_ENABLED`, `SUPABASE_API_PORT`) | no | +| `SUPABASE_EXPERIMENTAL` (or `--experimental`) | Fresh volume + no pg-delta: applies `db.migrations.schema_paths` files instead of `migrations/*.sql` (see "Fresh-volume DB setup" above) | no | +| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | Overrides the image registry used to resolve every service's image | no | +| `SUPABASE_PROJECT_ID` | Overrides the resolved local project id (env → config.toml → workdir basename) | no | +| `SUPABASE_WORKDIR` | Resolves `CommandSettings.workdir` | no | +| `SUPABASE_YES` (or `--yes`) | Auto-confirms the fresh-volume bucket-seed overwrite/prune prompts (shell or project dotenv, same as `seed buckets`) | no | +| `BITBUCKET_CLONE_DIR` | When non-empty, drops named volumes and `--security-opt` from every container create | no | +| `DOCKER_HOST` / `DOCKER_CONTEXT` / `DOCKER_TLS_VERIFY` / `DOCKER_CERT_PATH` / `DOCKER_API_VERSION` / `DOCKER_CONFIG` | Read (ambient shell OR a project `.env`/`.env.`/`.env.local` file) to discover the Docker daemon this whole command talks to; `DOCKER_HOST` is also re-derived and set on Vector's container env so it can reach the host's Docker socket for log collection | no | +| `KONG_NGINX_WORKER_PROCESSES` | Read (ambient shell or project dotenv) into Kong's own container env (defaults to `"1"` when unset) | no | +| `HTTP_PROXY` / `http_proxy` / `HTTPS_PROXY` / `https_proxy` / `NO_PROXY` / `no_proxy` | Bun proxy settings. After project dotenv and container creation, `start` appends `localhost,127.0.0.1,[::1]` to the effective no-proxy value before local Kong probes and seeding; it never changes project/container env and ends with this CLI process. | no | +| `SUPABASE_USE_SLIM_IMAGES` | Ambient `process.env` only (`true`/`1` enable) — not project dotenv. Rewrites current Dockerfile pins to `ghcr.io/supabase/cli/` (including PG15+ realtime/storage/auth migrate jobs). Kong, PG14, OrioleDB tags the catalog does not pin, historical Postgres pins, and `deno_version = 1` remain non-slim (still subject to `SUPABASE_INTERNAL_IMAGE_REGISTRY`). Majors 13/15 use the published slim PG15 pin (`15.14.1.167`) when the flag is on; flag-off keeps `15.8.1.085`. `SUPABASE_INTERNAL_IMAGE_REGISTRY` does not apply to slim refs. A failed GHCR pull falls back to the same tag and digest on `public.ecr.aws/supabase/cli/`. | no | `docker`/`podman` must be resolvable on `PATH` — same fallback behavior as `stop`/`status`. diff --git a/apps/cli/src/commands/start/start.slim-images.e2e.test.ts b/apps/cli/src/commands/start/start.slim-images.e2e.test.ts index 878efdbea1..d468ff1c84 100644 --- a/apps/cli/src/commands/start/start.slim-images.e2e.test.ts +++ b/apps/cli/src/commands/start/start.slim-images.e2e.test.ts @@ -2,7 +2,11 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http"; import { BunServices } from "@effect/platform-bun"; import { Clock, Crypto, Data, Effect, FileSystem, Layer, Path, Schema } from "effect"; import { beforeAll, describe, expect, it } from "@effect/vitest"; -import { catalogPins, type ArtifactKind } from "@supabase/stack/internal/artifacts"; +import { + catalogPins, + slimImageMirrors, + type ArtifactKind, +} from "@supabase/stack/internal/artifacts"; import { dockerfileServiceImageRaw } from "../../shared/services/dockerfile-images.ts"; import { isSlimImageRef, toSlimImage } from "../../shared/services/slim-images.ts"; @@ -115,6 +119,11 @@ const containerImage = Effect.fnUntraced(function* (name: string) { return stdout.trim(); }); +/* A slim image GHCR cannot serve comes from the ECR Public mirror at the same tag and digest. */ +const expectSlimContainerImage = Effect.fnUntraced(function* (name: string, expected: string) { + expect([expected, ...slimImageMirrors(expected)]).toContain(yield* containerImage(name)); +}); + /** `kong` has no slim build at all, so this stays fallback-tolerant for the pull-ahead list. */ function expectedSlimImage(alias: string): string { const raw = dockerfileServiceImageRaw(alias); @@ -307,7 +316,11 @@ describe("supabase start slim images (e2e)", () => { Effect.gen(function* () { for (const entry of catalogPins()) { if (!WGET_PROBE_SERVICES.includes(entry.service)) continue; - const image = entry.pin.image; + const image = yield* Effect.tryPromise({ + try: () => ensureImage(entry.pin.image), + catch: (cause) => + new StartE2eSetupError({ message: "failed to ensure Docker image", cause }), + }); const probe = entry.service === "realtime" ? slimWgetHealthcheck("http://127.0.0.1:9/", { @@ -380,15 +393,17 @@ describe("supabase start slim images (e2e)", () => { expect(image).toMatch(GHCR_SLIM_IMAGE_PATTERN); } - expect(yield* containerImage(dbContainer)).toBe( + yield* expectSlimContainerImage( + dbContainer, expectedPinnedImage("pg", dockerfileServiceImageRaw("pg")), ); - expect(yield* containerImage(storageContainer)).toBe(storageImage); - expect(yield* containerImage(edgeRuntimeContainer)).toBe( + yield* expectSlimContainerImage(storageContainer, storageImage); + yield* expectSlimContainerImage( + edgeRuntimeContainer, expectedPinnedImage("edgeruntime", dockerfileServiceImageRaw("edgeruntime")), ); - expect(yield* containerImage(authContainer)).toBe(authImage); - expect(yield* containerImage(realtimeContainer)).toBe(realtimeImage); + yield* expectSlimContainerImage(authContainer, authImage); + yield* expectSlimContainerImage(realtimeContainer, realtimeImage); expect(yield* containerHealthcheckTest(authContainer)).toEqual([ "CMD-SHELL", diff --git a/apps/cli/src/shared/services/slim-images.ts b/apps/cli/src/shared/services/slim-images.ts index e75706dcd5..f10dece518 100644 --- a/apps/cli/src/shared/services/slim-images.ts +++ b/apps/cli/src/shared/services/slim-images.ts @@ -1,8 +1,11 @@ -import { catalogPins } from "@supabase/stack/internal/artifacts"; +import { + catalogPins, + SLIM_IMAGE_GHCR_REGISTRY, + SLIM_IMAGE_SUPABASE_ECR_MIRROR, +} from "@supabase/stack/internal/artifacts"; import { Config, ConfigProvider, Effect, Option } from "effect"; const SLIM_IMAGES_ENV = "SUPABASE_USE_SLIM_IMAGES"; -const SLIM_IMAGE_PREFIX = "ghcr.io/supabase/cli/"; /** Dockerfile alias to slim service; kong, `pg14`, and the job images have no slim build. */ const SLIM_SERVICE_BY_ALIAS = { @@ -175,14 +178,16 @@ export function slimImageForCurrentPin( return toSlimImage(alias, tagged) ?? tagged; } -/** Slim images are published only under this prefix; single home for the check. */ +/** Slim images live on GHCR or its ECR Public mirror; single home for the check. */ export function isSlimImageRef(image: string): boolean { - return image.startsWith(SLIM_IMAGE_PREFIX); + return ( + image.startsWith(SLIM_IMAGE_GHCR_REGISTRY) || image.startsWith(SLIM_IMAGE_SUPABASE_ECR_MIRROR) + ); } /** - * True when the flag is on AND `image` is a slim ghcr ref. Spec builders and - * one-shot jobs use this so a ghcr-shaped override with the flag off stays on + * True when the flag is on AND `image` is a slim ref. Spec builders and + * one-shot jobs use this so a slim-shaped override with the flag off stays on * the docker.io contract. */ export function usesSlimImageRuntime(image: string, enabled: boolean): boolean { diff --git a/apps/cli/src/shared/services/slim-images.unit.test.ts b/apps/cli/src/shared/services/slim-images.unit.test.ts index 01bb586ad1..1f525976ac 100644 --- a/apps/cli/src/shared/services/slim-images.unit.test.ts +++ b/apps/cli/src/shared/services/slim-images.unit.test.ts @@ -17,42 +17,45 @@ import { // Only `auth` and one OrioleDB postgres build are pinned, so every other alias falls through to // its upstream image. `vi.mock` factories are hoisted above top-level statements, so the fixture // is inlined. -vi.mock("@supabase/stack/internal/artifacts", () => { - const digest = "d348483ad1141c54bfb4eaae801f5385fe1c2970fc106f95f531b5247092d52c"; - const nativePin = { archive: digest, manifest: digest }; - return { - catalogPins: () => [ - { - service: "auth", - sourceService: "auth", - pin: { - upstreamVersion: "v2.197.0", - revision: 0, - image: `ghcr.io/supabase/cli/auth:v2.197.0-r0@sha256:${digest}`, - natives: { - "darwin-arm64": nativePin, - "linux-amd64": nativePin, - "linux-arm64": nativePin, +vi.mock("@supabase/stack/internal/artifacts", (importOriginal) => + importOriginal().then((actual) => { + const digest = "d348483ad1141c54bfb4eaae801f5385fe1c2970fc106f95f531b5247092d52c"; + const nativePin = { archive: digest, manifest: digest }; + return { + ...actual, + catalogPins: () => [ + { + service: "auth", + sourceService: "auth", + pin: { + upstreamVersion: "v2.197.0", + revision: 0, + image: `ghcr.io/supabase/cli/auth:v2.197.0-r0@sha256:${digest}`, + natives: { + "darwin-arm64": nativePin, + "linux-amd64": nativePin, + "linux-arm64": nativePin, + }, }, }, - }, - { - service: "database", - sourceService: "postgres", - pin: { - upstreamVersion: "17.11.0.002-orioledb", - revision: 0, - image: `ghcr.io/supabase/cli/postgres:17.11.0.002-orioledb-r0@sha256:${digest}`, - natives: { - "darwin-arm64": nativePin, - "linux-amd64": nativePin, - "linux-arm64": nativePin, + { + service: "database", + sourceService: "postgres", + pin: { + upstreamVersion: "17.11.0.002-orioledb", + revision: 0, + image: `ghcr.io/supabase/cli/postgres:17.11.0.002-orioledb-r0@sha256:${digest}`, + natives: { + "darwin-arm64": nativePin, + "linux-amd64": nativePin, + "linux-arm64": nativePin, + }, }, }, - }, - ], - }; -}); + ], + }; + }), +); const AUTH_FIXTURE_PIN_IMAGE = "ghcr.io/supabase/cli/auth:v2.197.0-r0@sha256:d348483ad1141c54bfb4eaae801f5385fe1c2970fc106f95f531b5247092d52c"; @@ -262,7 +265,9 @@ describe("usesSlimImageRuntime", () => { it("is true only when the flag is on and the ref is slim", () => { expect(usesSlimImageRuntime("ghcr.io/supabase/cli/auth:v2.196.0", true)).toBe(true); + expect(usesSlimImageRuntime("public.ecr.aws/supabase/cli/auth:v2.196.0", true)).toBe(true); expect(usesSlimImageRuntime("supabase/gotrue:v2.196.0", true)).toBe(false); + expect(usesSlimImageRuntime("public.ecr.aws/supabase/gotrue:v2.196.0", true)).toBe(false); }); }); diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 6f3c1633d3..1c304459fe 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -602,7 +602,8 @@ const errorMessage = (cause: unknown): string => */ const SLIM_NATIVE_SUPABASE_S3_MIRROR = "https://supabase-cli-artifacts.s3.us-east-1.amazonaws.com"; -const SLIM_IMAGE_SUPABASE_ECR_MIRROR = "public.ecr.aws/supabase/cli/"; +/** ECR Public mirror of every slim image, under the same tags and digests. */ +export const SLIM_IMAGE_SUPABASE_ECR_MIRROR = "public.ecr.aws/supabase/cli/"; /** Mirrors carrying a catalog slim image under the same tag and digest, in fallback order. */ export const slimImageMirrors = (image: string): ReadonlyArray => diff --git a/packages/stack/src/internal/artifacts.ts b/packages/stack/src/internal/artifacts.ts index 5907e3bae5..16bca86003 100644 --- a/packages/stack/src/internal/artifacts.ts +++ b/packages/stack/src/internal/artifacts.ts @@ -12,6 +12,8 @@ export { postgresMajor, prepareNativeArtifact, resolveArtifact, + SLIM_IMAGE_SUPABASE_ECR_MIRROR, + slimImageMirrors, useNativeArtifact, type ArtifactKind, type ArtifactPin, @@ -19,6 +21,7 @@ export { type NativePin, type ServiceKind, } from "../Artifacts.ts"; +export { SLIM_IMAGE_GHCR_REGISTRY } from "../preparation/SlimEndpoints.ts"; export { ReleaseIntegrity, ReleaseTransient, From 99b45405ff2dae2db9d51df5b96c95dc3992a27a Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:27:40 +0000 Subject: [PATCH 32/33] feat(stack): forward client address headers to upstreams (CLI-2823) (#7345) ## TL;DR adds `X-Forwarded-For` and `X-Real-IP` to what the stack gateway sends upstream, so edge functions can see who is calling them ## whats introduced? the gateway now appends the client address to any incoming `X-Forwarded-For` chain and sets `X-Real-IP` to the client address, the same way the legacy backend does both the http path and the websocket upgrade path send them a client sent `X-Real-IP` is always replaced or dropped, so callers cant spoof it through the gateway
visual rep ```mermaid flowchart LR subgraph before A1[client] --> B1[stack gateway] B1 --> C1[edge function sees no client address] end subgraph now A2[client] --> B2[stack gateway] B2 --> D2{peer address known} D2 -->|yes| E2["append client to X-Forwarded-For
set X-Real-IP to client"] D2 -->|no| F2[drop client sent X-Real-IP] E2 --> G2[http and websocket upstreams] F2 --> G2 G2 --> H2[edge function sees the caller] end ```
## ref - closes: https://github.com/supabase/cli/issues/7275 --- .../stack/src/HttpProxy.integration.test.ts | 63 +++++++++++++++++++ packages/stack/src/HttpProxy.ts | 16 ++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/stack/src/HttpProxy.integration.test.ts b/packages/stack/src/HttpProxy.integration.test.ts index f193f157f2..f049201169 100644 --- a/packages/stack/src/HttpProxy.integration.test.ts +++ b/packages/stack/src/HttpProxy.integration.test.ts @@ -545,6 +545,69 @@ it.live("rewrites only the apikey query value on opted-in WebSocket routes", () ).pipe(Effect.provide(NodeServices.layer)), ); +it.live("tells upstreams the client address", () => + Effect.scoped( + Effect.gen(function* () { + const backend = createServer((incoming, response) => { + response.end( + JSON.stringify({ + forwardedFor: incoming.headers["x-forwarded-for"], + realIp: incoming.headers["x-real-ip"], + }), + ); + }); + const sockets = new WebSocketServer({ server: backend }); + const upgraded = yield* Deferred.make< + { forwardedFor?: string; realIp?: string }, + HttpProxyTestError + >(); + sockets.on("connection", (socket, incoming) => { + Deferred.doneUnsafe( + upgraded, + Effect.succeed({ + forwardedFor: headerOf(incoming.headers["x-forwarded-for"]), + realIp: headerOf(incoming.headers["x-real-ip"]), + }), + ); + socket.close(); + }); + const backendAddress = yield* listen(backend, { beforeClose: () => sockets.close() }); + const proxy = yield* makeHttpProxy({ host: "127.0.0.1", port: 0 }); + yield* proxy.setRoutes([ + { id: "functions", prefix: "/functions", target: Effect.succeed(backendAddress) }, + ]); + const send = (headers: Readonly>) => + request(proxy.port, "/functions/v1/hello", new Uint8Array(), headers, "GET").pipe( + Effect.provide(NodeHttpClient.layerNodeHttp), + Effect.map(({ body }) => JSON.parse(new TextDecoder().decode(body))), + ); + + expect(yield* send({})).toEqual({ forwardedFor: "127.0.0.1", realIp: "127.0.0.1" }); + expect(yield* send({ "x-forwarded-for": "203.0.113.7", "x-real-ip": "203.0.113.7" })).toEqual( + { forwardedFor: "203.0.113.7, 127.0.0.1", realIp: "127.0.0.1" }, + ); + + const client = new WebSocket(`ws://127.0.0.1:${proxy.port}/functions/v1/socket`, { + headers: { "x-forwarded-for": "203.0.113.7", "x-real-ip": "203.0.113.7" }, + }); + client.on("error", (cause) => + Deferred.doneUnsafe( + upgraded, + Effect.fail(new HttpProxyTestError({ message: cause.message, cause })), + ), + ); + yield* Effect.addFinalizer(() => Effect.sync(() => client.terminate())); + expect(yield* Deferred.await(upgraded).pipe(Effect.timeout("10 seconds"))).toEqual({ + forwardedFor: "203.0.113.7, 127.0.0.1", + realIp: "127.0.0.1", + }); + }), + ).pipe(Effect.provide(NodeServices.layer)), +); + +const headerOf = (value: string | ReadonlyArray | undefined) => + value === undefined || typeof value === "string" ? value : value.join(", "); + it.live("returns a gateway error naming the route and cause when a target cannot wake", () => { const logs: Array = []; return Effect.scoped( diff --git a/packages/stack/src/HttpProxy.ts b/packages/stack/src/HttpProxy.ts index 803196e53c..a1394e4358 100644 --- a/packages/stack/src/HttpProxy.ts +++ b/packages/stack/src/HttpProxy.ts @@ -129,11 +129,21 @@ const bearerValueFor = (headers: IncomingMessage["headers"], keys: HttpRouteKeyR return apiKey; }; -const upstreamHeadersFor = (headers: IncomingMessage["headers"], route: HttpRoute) => { +/* Like Kong: append the client to any incoming X-Forwarded-For chain; X-Real-IP is the client. */ +const upstreamHeadersFor = (request: IncomingMessage, route: HttpRoute) => { + const headers = request.headers; + const client = request.socket.remoteAddress; + const forwardedFor = headerValue(headers["x-forwarded-for"]); const result: Record = { ...headersFor(headers), ...(route.upstreamHost === undefined ? {} : { host: route.upstreamHost }), }; + /* Without a peer address, a client-sent X-Real-IP is still dropped rather than passed through. */ + if (client === undefined) delete result["x-real-ip"]; + else { + result["x-forwarded-for"] = forwardedFor === undefined ? client : `${forwardedFor}, ${client}`; + result["x-real-ip"] = client; + } const keyRewrite = route.keyRewrite; if (keyRewrite?.policy === "bearer") { const value = bearerValueFor(headers, keyRewrite.keys); @@ -393,7 +403,7 @@ const forward = Effect.fn("HttpProxy.forward")(function* ( // Bun ends a streamed upstream response early when the request says Connection: close, // and sends a GET or DELETE body unframed unless the request says it is chunked. headers: { - ...upstreamHeadersFor(request.headers, route), + ...upstreamHeadersFor(request, route), ...(request.headers["transfer-encoding"] === undefined ? {} : { "transfer-encoding": "chunked" }), @@ -565,7 +575,7 @@ const upgrade = Effect.fn("HttpProxy.upgrade")( client.once("end", onClientEnd); upstream.once("end", onUpstreamEnd); const lines = [`${request.method ?? "GET"} ${pathFor(request, route)} HTTP/1.1`]; - for (const [name, value] of Object.entries(upstreamHeadersFor(request.headers, route))) { + for (const [name, value] of Object.entries(upstreamHeadersFor(request, route))) { if (value !== undefined) lines.push(`${name}: ${Array.isArray(value) ? value.join(", ") : value}`); } From 730dec8339c8d3fcc0c11f4af36b34edfa1e443e Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:28:36 +0000 Subject: [PATCH 33/33] refactor(repo): cover `tools`, `apps/docs` and `cli-test-helpers` with effect lint (CLI-2881) (#7348) ## TL;DR brings `tools`, `apps/docs` and `packages/cli-test-helpers` under the effect lint ## whats introduced? - `apps/docs/**`, `packages/cli-test-helpers/**` and `tools/**` join the allow list - the release and config tools run as effect programs on `FileSystem` and `HttpClient`, with the same requests, console output, files written and exit codes, so the root gains `effect` and `@effect/platform-bun` dev dependencies - the test harness runs on effect too, so `cli-test-helpers` gains the same two as dependencies, and `makeTempDir` returns a promise of an async disposable dir, so its two `apps/cli-e2e` call sites await it - the docs page swaps its `async` functions for promise chains and renders the same pages - `local-registry.ts` adds one `oxlint-disable` for the `openSync` log descriptor Verdaccio shares across stdout and stderr, which effect's `ChildProcess` cannot hand over - a failed run prints the same error, with effect frames in the stack ## ref - closes: CLI-2881 --- .oxlintrc.effect.json | 3 + .../src/tests/project-lifecycle.e2e.test.ts | 4 +- apps/cli-e2e/src/tests/test-context.ts | 6 +- apps/docs/app/docs/[[...slug]]/page.tsx | 50 +-- package.json | 2 + packages/cli-test-helpers/package.json | 4 + packages/cli-test-helpers/src/harness.ts | 174 +++++--- pnpm-lock.yaml | 13 + tools/config-api-compare.ts | 377 ++++++++++------- tools/config-release-gate.ts | 380 ++++++++++++------ tools/lib/dts-diff.ts | 126 +++--- tools/release/local-registry.ts | 219 ++++++---- tools/release/local-release.ts | 291 +++++++++----- 13 files changed, 1063 insertions(+), 586 deletions(-) diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index 6964884c1d..ee9c33a8e9 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -10,6 +10,9 @@ "!packages/config/**", "!packages/stack/**", "!apps/cli/**", + "!apps/docs/**", + "!packages/cli-test-helpers/**", + "!tools/**", // Last match wins across the whole list: keep bare re-exclusions after every // `!` entry that would otherwise re-include them. "apps/cli/src/shared/cli/bin.ts", diff --git a/apps/cli-e2e/src/tests/project-lifecycle.e2e.test.ts b/apps/cli-e2e/src/tests/project-lifecycle.e2e.test.ts index 44cc611b30..9c68da3322 100644 --- a/apps/cli-e2e/src/tests/project-lifecycle.e2e.test.ts +++ b/apps/cli-e2e/src/tests/project-lifecycle.e2e.test.ts @@ -67,8 +67,8 @@ describe("link", () => { // requires the --project-ref flag. test("exits non-zero without --project-ref in non-TTY", async () => { const serverUrl = inject("replayServerUrl") as string; - const dir = makeTempDir("cli-e2e-link-no-ref-"); - using _ = dir; + const dir = await makeTempDir("cli-e2e-link-no-ref-"); + await using _ = dir; const harness = createHarness({ apiUrl: serverUrl, accessToken: ACCESS_TOKEN, diff --git a/apps/cli-e2e/src/tests/test-context.ts b/apps/cli-e2e/src/tests/test-context.ts index 4abaa73b84..4a856f97d6 100644 --- a/apps/cli-e2e/src/tests/test-context.ts +++ b/apps/cli-e2e/src/tests/test-context.ts @@ -59,7 +59,7 @@ export const testBehaviour = test.extend({ const serverUrl = inject("replayServerUrl"); const slug = scenarioSlug(task); // Truncate to 40 chars to keep temp dir names manageable. - const dir = makeTempDir(`cli-e2e-${slug.slice(0, 40)}-`); + const dir = await makeTempDir(`cli-e2e-${slug.slice(0, 40)}-`); const res = await fetch(`${serverUrl}/_ctrl/scenario`, { method: "POST", @@ -67,14 +67,14 @@ export const testBehaviour = test.extend({ body: JSON.stringify({ name: slug }), }); if (!res.ok) { - dir[Symbol.dispose](); + await dir[Symbol.asyncDispose](); const { message } = (await res.json()) as { message: string }; throw new Error(message); } await use(dir); - dir[Symbol.dispose](); + await dir[Symbol.asyncDispose](); await Promise.all([ fetch(`${serverUrl}/_ctrl/requests`, { method: "DELETE" }), fetch(`${serverUrl}/_ctrl/overrides`, { method: "DELETE" }), diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx index 689aa96a45..d749431ff2 100644 --- a/apps/docs/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/app/docs/[[...slug]]/page.tsx @@ -4,37 +4,39 @@ import { notFound } from "next/navigation"; import { source } from "@/lib/source"; import type { Metadata } from "next"; -export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); +export default function Page(props: { params: Promise<{ slug?: string[] }> }) { + return props.params.then((params) => { + const page = source.getPage(params.slug); + if (!page) notFound(); - const MDX = page.data.body; + const MDX = page.data.body; - return ( - - {page.data.title} - {page.data.description} - - - - - ); + return ( + + {page.data.title} + {page.data.description} + + + + + ); + }); } -export async function generateStaticParams() { - return source.generateParams(); +export function generateStaticParams() { + return Promise.resolve(source.generateParams()); } -export async function generateMetadata(props: { +export function generateMetadata(props: { params: Promise<{ slug?: string[] }>; }): Promise { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); + return props.params.then((params) => { + const page = source.getPage(params.slug); + if (!page) notFound(); - return { - title: page.data.title, - description: page.data.description, - }; + return { + title: page.data.title, + description: page.data.description, + }; + }); } diff --git a/package.json b/package.json index ca8e4bafd7..7000eee49d 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,11 @@ "devDependencies": { "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", + "@effect/platform-bun": "catalog:", "@effect/tsgo": "catalog:", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", + "effect": "catalog:", "husky": "^9.1.7", "knip": "catalog:", "oxfmt": "catalog:", diff --git a/packages/cli-test-helpers/package.json b/packages/cli-test-helpers/package.json index d05111f6d5..31fc757425 100644 --- a/packages/cli-test-helpers/package.json +++ b/packages/cli-test-helpers/package.json @@ -12,6 +12,10 @@ "test:unit:run": "bun --bun vitest run --project unit --coverage.reportsDirectory=coverage/unit", "types:check": "tsc --noEmit" }, + "dependencies": { + "@effect/platform-bun": "catalog:", + "effect": "catalog:" + }, "devDependencies": { "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", diff --git a/packages/cli-test-helpers/src/harness.ts b/packages/cli-test-helpers/src/harness.ts index 7e4fa5c416..740af46df5 100644 --- a/packages/cli-test-helpers/src/harness.ts +++ b/packages/cli-test-helpers/src/harness.ts @@ -1,5 +1,6 @@ -import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { Effect, FileSystem, Layer, Path, Schema } from "effect"; +import type { PlatformError } from "effect/PlatformError"; import { tmpdir, platform as osPlatform } from "node:os"; import { randomUUID } from "node:crypto"; @@ -34,19 +35,58 @@ export interface CLIHarness { /** A temporary directory that is removed when disposed. */ export interface TempDir { readonly path: string; - [Symbol.dispose](): void; + [Symbol.asyncDispose](): Promise; +} + +class MissingCliBuildError extends Schema.TaggedError()( + "MissingCliBuildError", + { message: Schema.String }, +) {} + +class SubprocessError extends Schema.TaggedError()("SubprocessError", { + cause: Schema.Defect(), +}) {} + +/* Rejects with each failure's underlying cause when it has one, and a plain Error for the build check, instead of the tagged Effect error. */ +function runHarness
( + effect: Effect.Effect< + A, + MissingCliBuildError | SubprocessError | PlatformError, + FileSystem.FileSystem | Path.Path + >, +): Promise { + return Effect.runPromise( + effect.pipe( + Effect.catchTags({ + MissingCliBuildError: (error) => Effect.die(new Error(error.message)), + SubprocessError: (error) => Effect.die(error.cause), + PlatformError: (error) => Effect.die(error.cause ?? error), + }), + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + ), + ); } /** Create a unique temporary directory under os.tmpdir() for use as a CLI * working directory. Dispose it after the test to clean up. */ -export function makeTempDir(prefix = "cli-e2e-"): TempDir { - const path = mkdtempSync(join(tmpdir(), prefix)); - return { - path, - [Symbol.dispose]() { - rmSync(path, { recursive: true, force: true }); - }, - }; +export function makeTempDir(prefix = "cli-e2e-"): Promise { + return runHarness( + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const { basename, dirname, join } = yield* Path.Path; + /* Prefixes tmpdir's own name, so every prefix, empty included, maps to mkdtemp(join(tmpdir(), prefix)). */ + const path = yield* fs.makeTempDirectory({ + directory: dirname(tmpdir()), + prefix: join(basename(tmpdir()), prefix), + }); + return { + path, + [Symbol.asyncDispose]() { + return runHarness(fs.remove(path, { recursive: true, force: true })); + }, + }; + }), + ); } // Resolve the monorepo root from this file's location: @@ -54,8 +94,6 @@ export function makeTempDir(prefix = "cli-e2e-"): TempDir { const WORKSPACE_ROOT = new URL("../../../", import.meta.url).pathname.replace(/\/$/, ""); const BINARY_EXT = osPlatform() === "win32" ? ".exe" : ""; -const TS_CLI_SHIM = join(WORKSPACE_ROOT, "apps/cli/dist/supabase.js"); -const TS_CLI_BINARY = join(WORKSPACE_ROOT, `apps/cli/dist/supabase${BINARY_EXT}`); // E2E subprocesses should only enter agent output mode when a test explicitly // opts in via `opts.env`. Keep this list aligned with @vercel/detect-agent env @@ -92,14 +130,26 @@ export function createSubprocessBaseEnv( return env; } -function assertTsCliBuilt(binaryPath: string): void { - if (!existsSync(TS_CLI_SHIM) || !existsSync(binaryPath)) { - throw new Error( - `Missing CLI build artifacts. Run \`pnpm --filter supabase build\` before running e2e tests.\n` + - ` expected shim: ${TS_CLI_SHIM}\n` + - ` expected binary: ${binaryPath}`, - ); - } +/* Treats any access failure, not only ENOENT, as missing. */ +function exists(fs: FileSystem.FileSystem, path: string): Effect.Effect { + return fs.exists(path).pipe(Effect.orElseSucceed(() => false)); +} + +function assertTsCliBuilt( + fs: FileSystem.FileSystem, + shimPath: string, + binaryPath: string, +): Effect.Effect { + return Effect.gen(function* () { + if (!(yield* exists(fs, shimPath)) || !(yield* exists(fs, binaryPath))) { + return yield* new MissingCliBuildError({ + message: + `Missing CLI build artifacts. Run \`pnpm --filter supabase build\` before running e2e tests.\n` + + ` expected shim: ${shimPath}\n` + + ` expected binary: ${binaryPath}`, + }); + } + }); } interface BuiltCommand { @@ -107,22 +157,30 @@ interface BuiltCommand { binaryOverride?: string; } -function buildCommand(): BuiltCommand { - assertTsCliBuilt(TS_CLI_BINARY); - return { cmd: ["node", TS_CLI_SHIM], binaryOverride: TS_CLI_BINARY }; +function readText(stream: ReadableStream): Effect.Effect { + return Effect.tryPromise({ + try: () => new Response(stream).text(), + catch: (cause) => new SubprocessError({ cause }), + }); } export function createHarness(options: HarnessOptions): CLIHarness { return { options }; } -export async function exec( +const execEffect = Effect.fnUntraced(function* ( harness: CLIHarness, args: string[], opts?: { env?: Record }, -): Promise { +) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + /* Builds the whole invocation before the first asynchronous step, in call order, so the caller + can mutate its inputs once exec returns. */ const start = performance.now(); - const built = buildCommand(); + const tsCliShim = path.join(WORKSPACE_ROOT, "apps/cli/dist/supabase.js"); + const tsCliBinary = path.join(WORKSPACE_ROOT, `apps/cli/dist/supabase${BINARY_EXT}`); + const built: BuiltCommand = { cmd: ["node", tsCliShim], binaryOverride: tsCliBinary }; const env: Record = { ...createSubprocessBaseEnv(), @@ -146,37 +204,53 @@ export async function exec( // file path, and the still-proxied Go binary reads the same file via // SUPABASE_PROFILE. Write a temp profile file pointing at the replay server so // both paths reach it. - const profilePath = join(tmpdir(), `cli-e2e-profile-${randomUUID()}.yaml`); + const profilePath = path.join(tmpdir(), `cli-e2e-profile-${randomUUID()}.yaml`); const url = harness.options.apiUrl; - writeFileSync( - profilePath, - [ - `name: test`, - `api_url: "${url}"`, - `dashboard_url: "${url}"`, - `project_host: ${harness.options.projectHost ?? "localhost"}`, - ].join("\n"), - ); + const profile = [ + `name: test`, + `api_url: "${url}"`, + `dashboard_url: "${url}"`, + `project_host: ${harness.options.projectHost ?? "localhost"}`, + ].join("\n"); + const command = [...built.cmd, ...args]; + // Default to os.tmpdir() so subprocess file writes never land in the repo + const cwd = harness.options.cwd ?? tmpdir(); + + yield* assertTsCliBuilt(fs, tsCliShim, tsCliBinary); + yield* fs.writeFileString(profilePath, profile); env["SUPABASE_PROFILE"] = profilePath; - const proc = Bun.spawn([...built.cmd, ...args], { - env, - // Default to os.tmpdir() so subprocess file writes never land in the repo - cwd: harness.options.cwd ?? tmpdir(), - stdout: "pipe", - stderr: "pipe", - stdin: "ignore", + const proc = yield* Effect.try({ + try: () => + Bun.spawn(command, { + env, + cwd, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }), + catch: (cause) => new SubprocessError({ cause }), }); - const [stdout, stderr] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - ]); + const [stdout, stderr] = yield* Effect.all([readText(proc.stdout), readText(proc.stderr)], { + concurrency: "unbounded", + }); - const exitCode = await proc.exited; + const exitCode = yield* Effect.tryPromise({ + try: () => proc.exited, + catch: (cause) => new SubprocessError({ cause }), + }); const durationMs = performance.now() - start; - if (profilePath) rmSync(profilePath, { force: true }); + if (profilePath) yield* fs.remove(profilePath, { force: true }); return { stdout, stderr, exitCode, durationMs }; +}); + +export function exec( + harness: CLIHarness, + args: string[], + opts?: { env?: Record }, +): Promise { + return runHarness(execEffect(harness, args, opts)); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0be1578fc..98eec27d69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,6 +248,9 @@ importers: '@commitlint/config-conventional': specifier: ^21.2.2 version: 21.2.2 + '@effect/platform-bun': + specifier: 'catalog:' + version: 4.0.2(effect@4.0.2) '@effect/tsgo': specifier: 'catalog:' version: 0.37.0 @@ -257,6 +260,9 @@ importers: '@types/bun': specifier: 'catalog:' version: 1.4.2 + effect: + specifier: 'catalog:' + version: 4.0.2 husky: specifier: ^9.1.7 version: 9.1.7 @@ -530,6 +536,13 @@ importers: packages/cli-linux-x64-musl: {} packages/cli-test-helpers: + dependencies: + '@effect/platform-bun': + specifier: 'catalog:' + version: 4.0.2(effect@4.0.2) + effect: + specifier: 'catalog:' + version: 4.0.2 devDependencies: '@tsconfig/bun': specifier: 'catalog:' diff --git a/tools/config-api-compare.ts b/tools/config-api-compare.ts index 5339d5b096..21fb1a894f 100644 --- a/tools/config-api-compare.ts +++ b/tools/config-api-compare.ts @@ -18,9 +18,18 @@ * Exit codes: 0 identical (or compare skipped), 1 surface differs, 2 tool failure. */ -import { cp, mkdir, mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { + Cause, + Config, + Console, + Effect, + FileSystem, + Layer, + Path, + PlatformError, + Schema, +} from "effect"; import process from "node:process"; import { parseArgs } from "node:util"; import { @@ -32,9 +41,21 @@ import { writeStepSummary, } from "./lib/dts-diff.ts"; -const repoRoot = path.resolve(import.meta.dir, ".."); -const packageRoot = path.join(repoRoot, "packages", "config"); -const tscBinPath = path.join(packageRoot, "node_modules", ".bin", "tsc"); +const toolPaths = Effect.gen(function* () { + const path = yield* Path.Path; + const repoRoot = path.resolve(import.meta.dir, ".."); + const packageRoot = path.join(repoRoot, "packages", "config"); + const tscBinPath = path.join(packageRoot, "node_modules", ".bin", "tsc"); + return { repoRoot, packageRoot, tscBinPath }; +}); + +class ConfigApiCompareError extends Schema.TaggedError()( + "ConfigApiCompareError", + { message: Schema.String }, +) {} + +const foreignFailure = (cause: unknown) => + new ConfigApiCompareError({ message: cause instanceof Error ? cause.message : String(cause) }); interface GitResult { readonly exitCode: number; @@ -42,34 +63,46 @@ interface GitResult { readonly stderr: string; } -async function runGit(args: readonly string[], cwd: string): Promise { - const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" }); - const [exitCode, stdout, stderr] = await Promise.all([ - proc.exited, - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - ]); +const runGit = Effect.fnUntraced(function* ( + args: readonly string[], + cwd: string, +): Effect.fn.Return { + const proc = yield* Effect.try({ + try: () => Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" }), + catch: foreignFailure, + }); + const [exitCode, stdout, stderr] = yield* Effect.tryPromise({ + try: () => + Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]), + catch: foreignFailure, + }); return { exitCode, stdout, stderr }; -} +}); -function requireBinaries(names: readonly string[]): void { +const requireBinaries = Effect.fnUntraced(function* (names: readonly string[]) { const missing = names.filter((name) => Bun.which(name) === null); if (missing.length > 0) { - throw new Error(`this tool requires ${missing.join(", ")} on PATH.`); + return yield* new ConfigApiCompareError({ + message: `this tool requires ${missing.join(", ")} on PATH.`, + }); } -} +}); /** `--base` wins; else `GITHUB_BASE_REF` (a PR's base branch name, no remote prefix) under `origin/`; else `origin/develop`. */ -function resolveBaseRef(cliBase: string | undefined): string { +const resolveBaseRef = Effect.fnUntraced(function* (cliBase: string | undefined) { if (cliBase) { return cliBase; } - const githubBaseRef = process.env.GITHUB_BASE_REF; + const githubBaseRef = yield* Config.String("GITHUB_BASE_REF").pipe(Config.withDefault("")); if (githubBaseRef) { return `origin/${githubBaseRef}`; } return "origin/develop"; -} +}); type MergeBaseResolution = | { readonly kind: "resolved"; readonly sha: string } @@ -79,56 +112,63 @@ type MergeBaseResolution = * Resolves `git merge-base HEAD `. Fetches a missing `origin/` ref at depth 1, * unshallowing the checkout if that still isn't enough to find a common ancestor. */ -async function resolveMergeBase(baseRef: string): Promise { - const attempt = await runGit(["merge-base", "HEAD", baseRef], repoRoot); +const resolveMergeBase = Effect.fnUntraced(function* ( + baseRef: string, +): Effect.fn.Return { + const { repoRoot } = yield* toolPaths; + const attempt = yield* runGit(["merge-base", "HEAD", baseRef], repoRoot); if (attempt.exitCode === 0) { return { kind: "resolved", sha: attempt.stdout.trim() }; } if (!baseRef.startsWith("origin/")) { - throw new Error( - `could not resolve base ref "${baseRef}" (git merge-base: ${attempt.stderr.trim()}). Pass a ` + + return yield* new ConfigApiCompareError({ + message: + `could not resolve base ref "${baseRef}" (git merge-base: ${attempt.stderr.trim()}). Pass a ` + `ref that already exists locally, or one under "origin/" so it can be fetched.`, - ); + }); } const branchName = baseRef.slice("origin/".length); - console.warn( + yield* Console.warn( `[config-api-compare] ${baseRef} did not resolve locally (${attempt.stderr.trim()}); fetching ` + `origin/${branchName} at depth 1...`, ); - const fetch = await runGit( + const fetch = yield* runGit( ["fetch", "--depth=1", "origin", `+${branchName}:refs/remotes/origin/${branchName}`], repoRoot, ); if (fetch.exitCode !== 0) { - throw new Error(`git fetch --depth=1 origin ${branchName} failed: ${fetch.stderr.trim()}`); + return yield* new ConfigApiCompareError({ + message: `git fetch --depth=1 origin ${branchName} failed: ${fetch.stderr.trim()}`, + }); } - const retry = await runGit(["merge-base", "HEAD", baseRef], repoRoot); + const retry = yield* runGit(["merge-base", "HEAD", baseRef], repoRoot); if (retry.exitCode === 0) { return { kind: "resolved", sha: retry.stdout.trim() }; } - const isShallow = await runGit(["rev-parse", "--is-shallow-repository"], repoRoot); + const isShallow = yield* runGit(["rev-parse", "--is-shallow-repository"], repoRoot); if (isShallow.stdout.trim() !== "true") { - throw new Error( - `could not resolve base ref "${baseRef}" even after fetching origin/${branchName}: ` + + return yield* new ConfigApiCompareError({ + message: + `could not resolve base ref "${baseRef}" even after fetching origin/${branchName}: ` + retry.stderr.trim(), - ); + }); } - console.warn( + yield* Console.warn( `[config-api-compare] HEAD's own checkout is shallow, so a depth-1 ${baseRef} fetch can't ` + "produce a common ancestor; unshallowing before retrying merge-base...", ); - const unshallow = await runGit(["fetch", "--unshallow", "origin", branchName], repoRoot); + const unshallow = yield* runGit(["fetch", "--unshallow", "origin", branchName], repoRoot); if (unshallow.exitCode !== 0) { - console.warn( + yield* Console.warn( `[config-api-compare] git fetch --unshallow failed (${unshallow.stderr.trim()}); falling ` + "back to git fetch --deepen=100000...", ); - const deepen = await runGit(["fetch", "--deepen=100000", "origin"], repoRoot); + const deepen = yield* runGit(["fetch", "--deepen=100000", "origin"], repoRoot); if (deepen.exitCode !== 0) { return { kind: "skip", @@ -139,7 +179,7 @@ async function resolveMergeBase(baseRef: string): Promise { } } - const fullFetch = await runGit( + const fullFetch = yield* runGit( ["fetch", "origin", `+${branchName}:refs/remotes/origin/${branchName}`], repoRoot, ); @@ -150,7 +190,7 @@ async function resolveMergeBase(baseRef: string): Promise { }; } - const finalRetry = await runGit(["merge-base", "HEAD", baseRef], repoRoot); + const finalRetry = yield* runGit(["merge-base", "HEAD", baseRef], repoRoot); if (finalRetry.exitCode === 0) { return { kind: "resolved", sha: finalRetry.stdout.trim() }; } @@ -161,57 +201,78 @@ async function resolveMergeBase(baseRef: string): Promise { `could not resolve a merge-base between HEAD and ${baseRef} even after unshallowing ` + `(git merge-base: ${finalRetry.stderr.trim()}).`, }; -} +}); -async function shortSha(rev: string): Promise { - const result = await runGit(["rev-parse", "--short", rev], repoRoot); +const shortSha = Effect.fnUntraced(function* (rev: string) { + const { repoRoot } = yield* toolPaths; + const result = yield* runGit(["rev-parse", "--short", rev], repoRoot); return result.exitCode === 0 ? result.stdout.trim() : rev; -} - -async function pathExistsAtRev(rev: string, relativePath: string): Promise { - const proc = Bun.spawn(["git", "cat-file", "-e", `${rev}:${relativePath}`], { - cwd: repoRoot, - stdout: "ignore", - stderr: "ignore", +}); + +const pathExistsAtRev = Effect.fnUntraced(function* (rev: string, relativePath: string) { + const { repoRoot } = yield* toolPaths; + const proc = yield* Effect.try({ + try: () => + Bun.spawn(["git", "cat-file", "-e", `${rev}:${relativePath}`], { + cwd: repoRoot, + stdout: "ignore", + stderr: "ignore", + }), + catch: foreignFailure, }); - return (await proc.exited) === 0; -} + return (yield* Effect.tryPromise({ try: () => proc.exited, catch: foreignFailure })) === 0; +}); /** * `git archive ` piped straight into `tar -x`, stripping * the shared `packages/config/` prefix (2 path components) so the extracted * tree lands directly under `destDir`. */ -async function archiveAndExtract( +const archiveAndExtract = Effect.fnUntraced(function* ( rev: string, relativePaths: readonly string[], destDir: string, -): Promise { - const archiveProc = Bun.spawn(["git", "archive", rev, ...relativePaths], { - cwd: repoRoot, - stdout: "pipe", - stderr: "pipe", +) { + const { repoRoot } = yield* toolPaths; + const archiveProc = yield* Effect.try({ + try: () => + Bun.spawn(["git", "archive", rev, ...relativePaths], { + cwd: repoRoot, + stdout: "pipe", + stderr: "pipe", + }), + catch: foreignFailure, + }); + const tarProc = yield* Effect.try({ + try: () => + Bun.spawn(["tar", "-x", "-C", destDir, "--strip-components=2"], { + stdin: archiveProc.stdout, + stdout: "pipe", + stderr: "pipe", + }), + catch: foreignFailure, }); - const tarProc = Bun.spawn(["tar", "-x", "-C", destDir, "--strip-components=2"], { - stdin: archiveProc.stdout, - stdout: "pipe", - stderr: "pipe", + const [archiveExitCode, tarExitCode, archiveStderr, tarStderr] = yield* Effect.tryPromise({ + try: () => + Promise.all([ + archiveProc.exited, + tarProc.exited, + new Response(archiveProc.stderr).text(), + new Response(tarProc.stderr).text(), + ]), + catch: foreignFailure, }); - const [archiveExitCode, tarExitCode, archiveStderr, tarStderr] = await Promise.all([ - archiveProc.exited, - tarProc.exited, - new Response(archiveProc.stderr).text(), - new Response(tarProc.stderr).text(), - ]); if (archiveExitCode !== 0) { - throw new Error( - `git archive ${rev} ${relativePaths.join(" ")} failed: ${archiveStderr.trim()}`, - ); + return yield* new ConfigApiCompareError({ + message: `git archive ${rev} ${relativePaths.join(" ")} failed: ${archiveStderr.trim()}`, + }); } if (tarExitCode !== 0) { - throw new Error(`tar extraction into ${destDir} failed: ${tarStderr.trim()}`); + return yield* new ConfigApiCompareError({ + message: `tar extraction into ${destDir} failed: ${tarStderr.trim()}`, + }); } -} +}); /** * Materializes the base revision's `packages/config/src` under `baseExtractDir`, inside @@ -219,37 +280,44 @@ async function archiveAndExtract( * stays the head copy so `extends` resolves; the declarations config uses the base's own copy * when present, else the head copy. */ -async function extractBaseTree(mergeBase: string, baseExtractDir: string): Promise { - await mkdir(baseExtractDir, { recursive: true }); +const extractBaseTree = Effect.fnUntraced(function* (mergeBase: string, baseExtractDir: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { packageRoot } = yield* toolPaths; + yield* fs.makeDirectory(baseExtractDir, { recursive: true }); const srcRelativePath = "packages/config/src"; const declarationsRelativePath = "packages/config/tsconfig.declarations.json"; - if (!(await pathExistsAtRev(mergeBase, srcRelativePath))) { - throw new Error(`base revision ${mergeBase} has no ${srcRelativePath} — cannot compare`); + if (!(yield* pathExistsAtRev(mergeBase, srcRelativePath))) { + return yield* new ConfigApiCompareError({ + message: `base revision ${mergeBase} has no ${srcRelativePath} — cannot compare`, + }); } - const hasDeclarationsConfig = await pathExistsAtRev(mergeBase, declarationsRelativePath); + const hasDeclarationsConfig = yield* pathExistsAtRev(mergeBase, declarationsRelativePath); const archivePaths = hasDeclarationsConfig ? [srcRelativePath, declarationsRelativePath] : [srcRelativePath]; - await archiveAndExtract(mergeBase, archivePaths, baseExtractDir); + yield* archiveAndExtract(mergeBase, archivePaths, baseExtractDir); - await cp( + yield* fs.copy( path.join(packageRoot, "tsconfig.build.json"), path.join(baseExtractDir, "tsconfig.build.json"), + { overwrite: true }, ); if (!hasDeclarationsConfig) { - console.warn( - `[config-api-compare] base revision ${await shortSha(mergeBase)} predates ` + + yield* Console.warn( + `[config-api-compare] base revision ${yield* shortSha(mergeBase)} predates ` + "tsconfig.declarations.json — using the HEAD copy for emit settings.", ); - await cp( + yield* fs.copy( path.join(packageRoot, "tsconfig.declarations.json"), path.join(baseExtractDir, "tsconfig.declarations.json"), + { overwrite: true }, ); } -} +}); interface EmitResult { readonly exitCode: number; @@ -264,24 +332,33 @@ interface EmitResult { * false, so declarations still emit against an old, non-type-checking base tree; only a fully * empty output counts as failure. */ -async function emitDeclarations( +const emitDeclarations = Effect.fnUntraced(function* ( projectPath: string, outDir: string, cwd: string, -): Promise { - const proc = Bun.spawn([tscBinPath, "-p", projectPath, "--outDir", outDir], { - cwd, - stdout: "pipe", - stderr: "pipe", +) { + const { tscBinPath } = yield* toolPaths; + const proc = yield* Effect.try({ + try: () => + Bun.spawn([tscBinPath, "-p", projectPath, "--outDir", outDir], { + cwd, + stdout: "pipe", + stderr: "pipe", + }), + catch: foreignFailure, + }); + const [exitCode, stdout, stderr] = yield* Effect.tryPromise({ + try: () => + Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]), + catch: foreignFailure, }); - const [exitCode, stdout, stderr] = await Promise.all([ - proc.exited, - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - ]); - const fileCount = await countDeclarationFiles(outDir); + const fileCount = yield* countDeclarationFiles(outDir); return { exitCode, stdout, stderr, fileCount }; -} +}); function renderTextReport(baseLabel: string, headLabel: string, result: CompareResult): string { const lines: string[] = [`Config type-surface diff: ${baseLabel} -> ${headLabel}`, ""]; @@ -354,81 +431,105 @@ function renderSkippedSummary(baseLabel: string, headLabel: string, baseEmit: Em ].join("\n"); } -async function main(): Promise { - requireBinaries(["git", "tar", "diff"]); +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { packageRoot } = yield* toolPaths; + yield* requireBinaries(["git", "tar", "diff"]); - const { values } = parseArgs({ options: { base: { type: "string" } } }); - const baseRef = resolveBaseRef(values.base); - const mergeBaseResolution = await resolveMergeBase(baseRef); + const { values } = yield* Effect.try({ + try: () => parseArgs({ options: { base: { type: "string" } } }), + catch: foreignFailure, + }); + const baseRef = yield* resolveBaseRef(values.base); + const mergeBaseResolution = yield* resolveMergeBase(baseRef); if (mergeBaseResolution.kind === "skip") { - const headLabel = await shortSha("HEAD"); - console.warn(`[config-api-compare] WARNING: ${mergeBaseResolution.reason}`); - await writeStepSummary( + const headLabel = yield* shortSha("HEAD"); + yield* Console.warn(`[config-api-compare] WARNING: ${mergeBaseResolution.reason}`); + yield* writeStepSummary( renderShallowHistorySkippedSummary(baseRef, headLabel, mergeBaseResolution.reason), ); return 0; } const mergeBase = mergeBaseResolution.sha; - const [baseLabel, headLabel] = await Promise.all([shortSha(mergeBase), shortSha("HEAD")]); - console.log( + const [baseLabel, headLabel] = yield* Effect.all([shortSha(mergeBase), shortSha("HEAD")], { + concurrency: "unbounded", + }); + yield* Console.log( `[config-api-compare] comparing merge-base ${baseLabel} (of ${baseRef}) against HEAD ${headLabel}...`, ); const compareDir = path.join(packageRoot, ".api-compare"); const baseExtractDir = path.join(compareDir, "base"); - const headOutDir = await mkdtemp(path.join(tmpdir(), "supabase-config-api-compare-head-")); - const baseOutDir = await mkdtemp(path.join(tmpdir(), "supabase-config-api-compare-base-")); + const headOutDir = yield* fs.makeTempDirectory({ prefix: "supabase-config-api-compare-head-" }); + const baseOutDir = yield* fs.makeTempDirectory({ prefix: "supabase-config-api-compare-base-" }); - try { - await rm(compareDir, { recursive: true, force: true }); + const exit = yield* Effect.gen(function* () { + yield* fs.remove(compareDir, { recursive: true, force: true }); - console.log("[config-api-compare] emitting head declarations..."); - const headEmit = await emitDeclarations( + yield* Console.log("[config-api-compare] emitting head declarations..."); + const headEmit = yield* emitDeclarations( path.join(packageRoot, "tsconfig.declarations.json"), headOutDir, packageRoot, ); if (headEmit.fileCount === 0) { - throw new Error( - `head declaration emit produced zero .d.ts files (tsc exit ${headEmit.exitCode}):\n` + + return yield* new ConfigApiCompareError({ + message: + `head declaration emit produced zero .d.ts files (tsc exit ${headEmit.exitCode}):\n` + `${headEmit.stdout}\n${headEmit.stderr}`, - ); + }); } - console.log("[config-api-compare] extracting and emitting base declarations..."); - await extractBaseTree(mergeBase, baseExtractDir); - const baseEmit = await emitDeclarations( + yield* Console.log("[config-api-compare] extracting and emitting base declarations..."); + yield* extractBaseTree(mergeBase, baseExtractDir); + const baseEmit = yield* emitDeclarations( path.join(baseExtractDir, "tsconfig.declarations.json"), baseOutDir, baseExtractDir, ); if (baseEmit.fileCount === 0) { - console.warn( + yield* Console.warn( `[config-api-compare] WARNING: base declaration emit produced zero .d.ts files (tsc exit ` + `${baseEmit.exitCode}) — skipping the compare rather than reporting a false surface diff.\n` + baseEmit.stderr, ); - await writeStepSummary(renderSkippedSummary(baseLabel, headLabel, baseEmit)); + yield* writeStepSummary(renderSkippedSummary(baseLabel, headLabel, baseEmit)); return 0; } - const result = await diffDeclarationTrees(headOutDir, baseOutDir); - console.log(renderTextReport(baseLabel, headLabel, result)); - await writeStepSummary(renderMarkdownSummary(baseLabel, headLabel, result)); + const result = yield* diffDeclarationTrees(headOutDir, baseOutDir); + yield* Console.log(renderTextReport(baseLabel, headLabel, result)); + yield* writeStepSummary(renderMarkdownSummary(baseLabel, headLabel, result)); return result.identical ? 0 : 1; - } finally { - await Promise.all([ - rm(compareDir, { recursive: true, force: true }), - rm(headOutDir, { recursive: true, force: true }), - rm(baseOutDir, { recursive: true, force: true }), - ]); - } -} - -try { - process.exit(await main()); -} catch (error) { - console.error(`[config-api-compare] ${error instanceof Error ? error.message : String(error)}`); - process.exit(2); -} + }).pipe(Effect.exit); + yield* Effect.all( + [ + fs.remove(compareDir, { recursive: true, force: true }), + fs.remove(headOutDir, { recursive: true, force: true }), + fs.remove(baseOutDir, { recursive: true, force: true }), + ], + { concurrency: "unbounded" }, + ); + return yield* exit; +}); + +process.exit( + await Effect.runPromise( + main.pipe( + Effect.catchCause((cause) => { + const error = Cause.squash(cause); + /* Prints a PlatformError's underlying cause message when it has one, not the PlatformError summary. */ + const reported = + PlatformError.isPlatformError(error) && error.cause instanceof Error + ? error.cause + : error; + return Console.error( + `[config-api-compare] ${reported instanceof Error ? reported.message : String(reported)}`, + ).pipe(Effect.as(2)); + }), + Effect.provide(Layer.mergeAll(BunFileSystem.layer, BunPath.layer)), + ), + ), +); diff --git a/tools/config-release-gate.ts b/tools/config-release-gate.ts index b26b8b01cf..dab4a93458 100644 --- a/tools/config-release-gate.ts +++ b/tools/config-release-gate.ts @@ -25,9 +25,19 @@ * failure. */ -import { mkdir, mkdtemp, readFile, rm, stat } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { + Cause, + Console, + Effect, + FileSystem, + Layer, + Path, + PlatformError, + Schema, + Stream, +} from "effect"; +import { FetchHttpClient, HttpClient, type HttpClientError } from "effect/http"; import process from "node:process"; import { parseArgs } from "node:util"; import { @@ -36,6 +46,7 @@ import { countDeclarationFiles, diffDeclarationTrees, type FileEntry, + readUtf8, renderDiffDetailsBlocks, writeStepSummary, } from "./lib/dts-diff.ts"; @@ -43,9 +54,30 @@ import { const PACKAGE_NAME = "@supabase/config"; const DEFAULT_REGISTRY = "https://registry.npmjs.org"; -const repoRoot = path.resolve(import.meta.dir, ".."); -const packageRoot = path.join(repoRoot, "packages", "config"); -const localDistDir = path.join(packageRoot, "dist"); +const toolPaths = Effect.gen(function* () { + const path = yield* Path.Path; + const repoRoot = path.resolve(import.meta.dir, ".."); + const packageRoot = path.join(repoRoot, "packages", "config"); + const localDistDir = path.join(packageRoot, "dist"); + return { repoRoot, localDistDir }; +}); + +class ConfigReleaseGateError extends Schema.TaggedError()( + "ConfigReleaseGateError", + { message: Schema.String }, +) {} + +const foreignFailure = (cause: unknown) => + new ConfigReleaseGateError({ message: cause instanceof Error ? cause.message : String(cause) }); + +const UnknownFromJsonString = Schema.fromJsonString(Schema.Unknown); + +/* Native `JSON.parse`, so a malformed package.json fails with its own `SyntaxError`. */ +const parseJson = (text: string): unknown => JSON.parse(text); + +/* Reports the underlying fetch error's message, not the HttpClientError summary. */ +const httpFailure = (error: HttpClientError.HttpClientError) => + foreignFailure(error.cause ?? error); interface CommandResult { readonly exitCode: number; @@ -53,48 +85,58 @@ interface CommandResult { readonly stderr: string; } -async function runCommand(cmd: readonly string[]): Promise { - const proc = Bun.spawn([...cmd], { cwd: repoRoot, stdout: "pipe", stderr: "pipe" }); - const [exitCode, stdout, stderr] = await Promise.all([ - proc.exited, - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - ]); +const runCommand = Effect.fnUntraced(function* ( + cmd: readonly string[], +): Effect.fn.Return { + const { repoRoot } = yield* toolPaths; + const proc = yield* Effect.try({ + try: () => Bun.spawn([...cmd], { cwd: repoRoot, stdout: "pipe", stderr: "pipe" }), + catch: foreignFailure, + }); + const [exitCode, stdout, stderr] = yield* Effect.tryPromise({ + try: () => + Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]), + catch: foreignFailure, + }); return { exitCode, stdout, stderr }; -} +}); -function requireBinaries(names: readonly string[]): void { +const requireBinaries = Effect.fnUntraced(function* (names: readonly string[]) { const missing = names.filter((name) => Bun.which(name) === null); if (missing.length > 0) { - throw new Error(`this tool requires ${missing.join(", ")} on PATH.`); + return yield* new ConfigReleaseGateError({ + message: `this tool requires ${missing.join(", ")} on PATH.`, + }); } -} +}); /** * Reads the ambient npm registry so a local Verdaccio harness, which rewrites npm's registry * config, is honored instead of assuming the public registry. */ -async function ambientNpmRegistry(): Promise { - const result = await runCommand(["npm", "config", "get", "registry"]); +const ambientNpmRegistry = Effect.fnUntraced(function* () { + const result = yield* runCommand(["npm", "config", "get", "registry"]); const registry = result.stdout.trim().replace(/\/+$/, ""); return result.exitCode === 0 && registry !== "" ? registry : DEFAULT_REGISTRY; -} +}); -async function pathExists(target: string): Promise { - try { - await stat(target); - return true; - } catch { - return false; - } -} +const pathExists = (fs: FileSystem.FileSystem, target: string) => + fs.stat(target).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ); -async function extractTarball(tarballPath: string, destDir: string): Promise { - await mkdir(destDir, { recursive: true }); +const extractTarball = Effect.fnUntraced(function* (tarballPath: string, destDir: string) { + const fs = yield* FileSystem.FileSystem; + yield* fs.makeDirectory(destDir, { recursive: true }); // The tarball is registry-supplied, i.e. untrusted: never restore its // recorded owners or permission bits. (GNU tar already refuses `..` // members, so path escape is covered by the extractor itself.) - const result = await runCommand([ + const result = yield* runCommand([ "tar", "-xzf", tarballPath, @@ -104,81 +146,109 @@ async function extractTarball(tarballPath: string, destDir: string): Promise { return typeof value === "object" && value !== null; } /** Reads the `version` field out of an extracted tarball's `package/package.json`. */ -async function readExtractedPackageVersion(extractDir: string): Promise { +const readExtractedPackageVersion = Effect.fnUntraced(function* (extractDir: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; const packageJsonPath = path.join(extractDir, "package", "package.json"); - const raw = await readFile(packageJsonPath, "utf8"); - const parsed: unknown = JSON.parse(raw); + const raw = yield* readUtf8(fs, packageJsonPath); + const parsed = yield* Effect.try({ try: () => parseJson(raw), catch: foreignFailure }); if (!isRecord(parsed) || typeof parsed.version !== "string") { - throw new Error(`${packageJsonPath} has no string "version" field.`); + return yield* new ConfigReleaseGateError({ + message: `${packageJsonPath} has no string "version" field.`, + }); } return parsed.version; -} - -async function downloadFile(url: string, destPath: string): Promise { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`failed to download ${url}: HTTP ${response.status}`); - } - await Bun.write(destPath, response); -} +}); + +const downloadFile = Effect.fnUntraced(function* (url: string, destPath: string) { + const fs = yield* FileSystem.FileSystem; + const client = yield* HttpClient.HttpClient; + const response = yield* client.get(url).pipe(Effect.mapError(httpFailure)); + if (response.status < 200 || response.status >= 300) { + return yield* new ConfigReleaseGateError({ + message: `failed to download ${url}: HTTP ${response.status}`, + }); + } + yield* response.stream.pipe( + /* A body-less 2xx (204/205) has no stream, and still leaves an empty file. */ + Stream.catchReason("HttpClientError", "EmptyBodyError", () => Stream.empty), + Stream.mapError(httpFailure), + Stream.run(fs.sink(destPath)), + ); +}); /** * `npm view --json` reports errors as `{"error":{"code":…}}` on stdout, so a * genuine E404 (never published) is distinguishable from a transport failure * or an error message that merely mentions "E404" somewhere. */ -async function npmViewJson( +const npmViewJson = Effect.fnUntraced(function* ( spec: string, field: string, registry: string, -): Promise< - { readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly code: string } +): Effect.fn.Return< + { readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly code: string }, + ConfigReleaseGateError, + Path.Path > { - const result = await runCommand(["npm", "view", spec, field, "--registry", registry, "--json"]); - let parsed: unknown; - try { - // `npm view` prints nothing at all for an absent field. - parsed = result.stdout.trim() === "" ? undefined : JSON.parse(result.stdout); - } catch { - throw new Error( - `npm view ${spec} ${field} --registry ${registry} returned unparseable output: ${result.stdout.trim().slice(0, 200)}`, - ); - } + const result = yield* runCommand(["npm", "view", spec, field, "--registry", registry, "--json"]); + // `npm view` prints nothing at all for an absent field. + const parsed: unknown = + result.stdout.trim() === "" + ? undefined + : yield* Schema.decodeEffect(UnknownFromJsonString)(result.stdout).pipe( + Effect.mapError( + () => + new ConfigReleaseGateError({ + message: `npm view ${spec} ${field} --registry ${registry} returned unparseable output: ${result.stdout.trim().slice(0, 200)}`, + }), + ), + ); if (result.exitCode !== 0) { if (isRecord(parsed) && isRecord(parsed.error) && typeof parsed.error.code === "string") { return { ok: false, code: parsed.error.code }; } - throw new Error( - `npm view ${spec} ${field} --registry ${registry} failed: ${result.stderr.trim()}`, - ); + return yield* new ConfigReleaseGateError({ + message: `npm view ${spec} ${field} --registry ${registry} failed: ${result.stderr.trim()}`, + }); } return { ok: true, value: parsed }; -} +}); -async function verifyTarballIntegrity(tarballPath: string, integrity: string): Promise { +const verifyTarballIntegrity = Effect.fnUntraced(function* ( + tarballPath: string, + integrity: string, +) { if (!integrity.startsWith("sha512-")) { - throw new Error(`expected a sha512 integrity value from the registry, got "${integrity}".`); + return yield* new ConfigReleaseGateError({ + message: `expected a sha512 integrity value from the registry, got "${integrity}".`, + }); } const hasher = new Bun.CryptoHasher("sha512"); - hasher.update(await Bun.file(tarballPath).arrayBuffer()); + hasher.update( + yield* Effect.tryPromise({ + try: () => Bun.file(tarballPath).arrayBuffer(), + catch: foreignFailure, + }), + ); const digest = `sha512-${hasher.digest("base64")}`; if (digest !== integrity) { - throw new Error( - `downloaded tarball failed integrity verification: registry says ${integrity}, got ${digest}.`, - ); + return yield* new ConfigReleaseGateError({ + message: `downloaded tarball failed integrity verification: registry says ${integrity}, got ${digest}.`, + }); } -} +}); type PublishedSide = | { readonly kind: "first-publish" } @@ -189,26 +259,33 @@ type PublishedSide = * `dist-tags.latest` from ``, verified against its `dist.integrity` and refused if its * URL points off-registry. No published version (or no `latest` tag) resolves to `"first-publish"`. */ -async function resolvePublishedSide( +const resolvePublishedSide = Effect.fnUntraced(function* ( registry: string, tarballArg: string | undefined, extractDir: string, -): Promise { +): Effect.fn.Return< + PublishedSide, + ConfigReleaseGateError | PlatformError.PlatformError, + FileSystem.FileSystem | Path.Path | HttpClient.HttpClient +> { + const path = yield* Path.Path; if (tarballArg) { - await extractTarball(tarballArg, extractDir); - const version = await readExtractedPackageVersion(extractDir); + yield* extractTarball(tarballArg, extractDir); + const version = yield* readExtractedPackageVersion(extractDir); return { kind: "resolved", version, distDir: path.join(extractDir, "package", "dist") }; } - const latestResult = await npmViewJson(PACKAGE_NAME, "dist-tags.latest", registry); + const latestResult = yield* npmViewJson(PACKAGE_NAME, "dist-tags.latest", registry); if (!latestResult.ok) { if (latestResult.code === "E404") { return { kind: "first-publish" }; } - throw new Error(`npm view ${PACKAGE_NAME} dist-tags.latest failed with ${latestResult.code}.`); + return yield* new ConfigReleaseGateError({ + message: `npm view ${PACKAGE_NAME} dist-tags.latest failed with ${latestResult.code}.`, + }); } if (typeof latestResult.value !== "string" || latestResult.value === "") { - console.warn( + yield* Console.warn( `[config-release-gate] ${PACKAGE_NAME} exists on ${registry} but has no "latest" dist-tag — treating as first publish.`, ); return { kind: "first-publish" }; @@ -216,34 +293,42 @@ async function resolvePublishedSide( const latestVersion = latestResult.value; const spec = `${PACKAGE_NAME}@${latestVersion}`; - const [tarballUrlResult, integrityResult] = await Promise.all([ - npmViewJson(spec, "dist.tarball", registry), - npmViewJson(spec, "dist.integrity", registry), - ]); + const [tarballUrlResult, integrityResult] = yield* Effect.all( + [npmViewJson(spec, "dist.tarball", registry), npmViewJson(spec, "dist.integrity", registry)], + { concurrency: "unbounded" }, + ); if (!tarballUrlResult.ok || typeof tarballUrlResult.value !== "string") { - throw new Error(`npm view ${spec} dist.tarball returned no tarball URL.`); + return yield* new ConfigReleaseGateError({ + message: `npm view ${spec} dist.tarball returned no tarball URL.`, + }); } if (!integrityResult.ok || typeof integrityResult.value !== "string") { - throw new Error(`npm view ${spec} dist.integrity returned no integrity value.`); + return yield* new ConfigReleaseGateError({ + message: `npm view ${spec} dist.integrity returned no integrity value.`, + }); } const tarballUrl = tarballUrlResult.value; - if (new URL(tarballUrl).origin !== new URL(registry).origin) { - throw new Error( - `refusing tarball from ${new URL(tarballUrl).origin} — it does not match the registry origin ${new URL(registry).origin}.`, - ); + const [tarballOrigin, registryOrigin] = yield* Effect.try({ + try: () => [new URL(tarballUrl).origin, new URL(registry).origin], + catch: foreignFailure, + }); + if (tarballOrigin !== registryOrigin) { + return yield* new ConfigReleaseGateError({ + message: `refusing tarball from ${tarballOrigin} — it does not match the registry origin ${registryOrigin}.`, + }); } const downloadedTarballPath = path.join(extractDir, "published.tgz"); - await downloadFile(tarballUrl, downloadedTarballPath); - await verifyTarballIntegrity(downloadedTarballPath, integrityResult.value); - await extractTarball(downloadedTarballPath, extractDir); + yield* downloadFile(tarballUrl, downloadedTarballPath); + yield* verifyTarballIntegrity(downloadedTarballPath, integrityResult.value); + yield* extractTarball(downloadedTarballPath, extractDir); return { kind: "resolved", version: latestVersion, distDir: path.join(extractDir, "package", "dist"), }; -} +}); type BumpClass = "major" | "minor" | "patch" | "none" | "downgrade" | "unknown"; @@ -424,46 +509,56 @@ function renderFirstPublishSummary(nextVersion: string): string { ].join("\n"); } -async function main(): Promise { - const { values } = parseArgs({ - options: { - version: { type: "string" }, - registry: { type: "string" }, - tarball: { type: "string" }, - "local-dist": { type: "string" }, - }, +const main = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const { localDistDir } = yield* toolPaths; + const { values } = yield* Effect.try({ + try: () => + parseArgs({ + options: { + version: { type: "string" }, + registry: { type: "string" }, + tarball: { type: "string" }, + "local-dist": { type: "string" }, + }, + }), + catch: foreignFailure, }); if (!values.version) { - throw new Error("--version is required."); + return yield* new ConfigReleaseGateError({ message: "--version is required." }); } const nextVersion = values.version; - requireBinaries(values.tarball ? ["tar", "diff"] : ["tar", "diff", "npm"]); + yield* requireBinaries(values.tarball ? ["tar", "diff"] : ["tar", "diff", "npm"]); const registry = - values.registry ?? (values.tarball ? DEFAULT_REGISTRY : await ambientNpmRegistry()); + values.registry ?? (values.tarball ? DEFAULT_REGISTRY : yield* ambientNpmRegistry()); const localDist = values["local-dist"] ?? localDistDir; - if (!(await pathExists(localDist)) || (await countDeclarationFiles(localDist)) === 0) { - throw new Error( - `${localDist} has no .d.ts files — ` + + if (!(yield* pathExists(fs, localDist)) || (yield* countDeclarationFiles(localDist)) === 0) { + return yield* new ConfigReleaseGateError({ + message: + `${localDist} has no .d.ts files — ` + (values["local-dist"] ? "the packed tarball lost its declarations (the .gitignore/packlist trap packages/config/.npmignore exists for)." : "run the package build first (e.g. `pnpm exec turbo run @supabase/config#build`)."), - ); + }); } - const extractDir = await mkdtemp(path.join(tmpdir(), "supabase-config-release-gate-")); + const extractDir = yield* fs.makeTempDirectory({ prefix: "supabase-config-release-gate-" }); - try { - const published = await resolvePublishedSide(registry, values.tarball, extractDir); + const exit = yield* Effect.gen(function* () { + const published = yield* resolvePublishedSide(registry, values.tarball, extractDir); if (published.kind === "first-publish") { - console.log(`[config-release-gate] ${PACKAGE_NAME} has never been published to ${registry}.`); - console.log(FIRST_PUBLISH_MESSAGE); - await writeStepSummary(renderFirstPublishSummary(nextVersion)); + yield* Console.log( + `[config-release-gate] ${PACKAGE_NAME} has never been published to ${registry}.`, + ); + yield* Console.log(FIRST_PUBLISH_MESSAGE); + yield* writeStepSummary(renderFirstPublishSummary(nextVersion)); return 0; } @@ -472,35 +567,56 @@ async function main(): Promise { // full surface (as additions) rather than approving sight unseen. let publishedDistDir = published.distDir; const extraWarnings: string[] = []; - if ((await countDeclarationFiles(publishedDistDir)) === 0) { + if ((yield* countDeclarationFiles(publishedDistDir)) === 0) { const message = `published ${published.version} tarball contains no .d.ts files — no baseline to diff ` + "against; the next release's full surface is shown below as additions."; - console.warn(`[config-release-gate] ${message}`); + yield* Console.warn(`[config-release-gate] ${message}`); extraWarnings.push(message); publishedDistDir = path.join(extractDir, "empty-published-dist"); - await mkdir(publishedDistDir, { recursive: true }); + yield* fs.makeDirectory(publishedDistDir, { recursive: true }); } - console.log( + yield* Console.log( `[config-release-gate] comparing published ${published.version} against next ${nextVersion}...`, ); - const result = await diffDeclarationTrees(localDist, publishedDistDir); + const result = yield* diffDeclarationTrees(localDist, publishedDistDir); const bumpClass = computeBumpClass(published.version, nextVersion); const warnings = [...extraWarnings, ...computeWarnings(bumpClass, result)]; - console.log(renderTextReport(published.version, nextVersion, result, warnings)); - await writeStepSummary(renderMarkdownSummary(published.version, nextVersion, result, warnings)); + yield* Console.log(renderTextReport(published.version, nextVersion, result, warnings)); + yield* writeStepSummary( + renderMarkdownSummary(published.version, nextVersion, result, warnings), + ); return 0; - } finally { - await rm(extractDir, { recursive: true, force: true }); - } -} - -try { - process.exit(await main()); -} catch (error) { - console.error(`[config-release-gate] ${error instanceof Error ? error.message : String(error)}`); - process.exit(2); -} + }).pipe(Effect.exit); + yield* fs.remove(extractDir, { recursive: true, force: true }); + return yield* exit; +}); + +process.exit( + await Effect.runPromise( + main.pipe( + Effect.catchCause((cause) => { + const error = Cause.squash(cause); + /* Prints a PlatformError's underlying cause message when it has one, not the PlatformError summary. */ + const reported = + PlatformError.isPlatformError(error) && error.cause instanceof Error + ? error.cause + : error; + return Console.error( + `[config-release-gate] ${reported instanceof Error ? reported.message : String(reported)}`, + ).pipe(Effect.as(2)); + }), + Effect.provide( + Layer.mergeAll( + BunFileSystem.layer, + BunPath.layer, + FetchHttpClient.layer, + Layer.succeed(HttpClient.TracerPropagationEnabled, false), + ), + ), + ), + ), +); diff --git a/tools/lib/dts-diff.ts b/tools/lib/dts-diff.ts index 23d8b8be3e..1ea37bfe24 100644 --- a/tools/lib/dts-diff.ts +++ b/tools/lib/dts-diff.ts @@ -6,56 +6,73 @@ * built `dist/`). */ -import { appendFile, readFile, stat } from "node:fs/promises"; -import path from "node:path"; -import process from "node:process"; +import { Config, Effect, FileSystem, Path, Schema } from "effect"; -/** `Bun.Glob.scan` throws ENOENT on a missing directory; a missing tree means "no declarations", not a tool failure. */ -async function directoryExists(dir: string): Promise { - try { - return (await stat(dir)).isDirectory(); - } catch { - return false; - } -} +class DtsDiffError extends Schema.TaggedError()("DtsDiffError", { + message: Schema.String, +}) {} -export async function countDeclarationFiles(dir: string): Promise { - if (!(await directoryExists(dir))) { +const foreignFailure = (cause: unknown) => + new DtsDiffError({ message: cause instanceof Error ? cause.message : String(cause) }); + +/** `Bun.Glob.scan` throws ENOENT on a missing directory; a missing tree means "no declarations", not a tool failure. */ +const directoryExists = (fs: FileSystem.FileSystem, dir: string) => + fs.stat(dir).pipe( + Effect.map((info) => info.type === "Directory"), + Effect.orElseSucceed(() => false), + ); + +export const countDeclarationFiles = Effect.fnUntraced(function* (dir: string) { + const fs = yield* FileSystem.FileSystem; + if (!(yield* directoryExists(fs, dir))) { return 0; } const glob = new Bun.Glob("**/*.d.ts"); - let count = 0; - for await (const _relativePath of glob.scan({ cwd: dir })) { - count++; - } - return count; -} + const relativePaths = yield* Effect.tryPromise({ + try: () => Array.fromAsync(glob.scan({ cwd: dir })), + catch: foreignFailure, + }); + return relativePaths.length; +}); -async function listDeclarationFiles(dir: string): Promise { - if (!(await directoryExists(dir))) { +const listDeclarationFiles = Effect.fnUntraced(function* (fs: FileSystem.FileSystem, dir: string) { + if (!(yield* directoryExists(fs, dir))) { return []; } const glob = new Bun.Glob("**/*.d.ts"); - const relativePaths: string[] = []; - for await (const relativePath of glob.scan({ cwd: dir })) { - relativePaths.push(relativePath); - } + const relativePaths = yield* Effect.tryPromise({ + try: () => Array.fromAsync(glob.scan({ cwd: dir })), + catch: foreignFailure, + }); return relativePaths.sort(); -} +}); -async function unifiedDiff( +/* `ignoreBOM` keeps a leading BOM, which `readFileString` would strip. */ +export const readUtf8 = (fs: FileSystem.FileSystem, filePath: string) => + fs + .readFile(filePath) + .pipe(Effect.map((bytes) => new TextDecoder("utf-8", { ignoreBOM: true }).decode(bytes))); + +const unifiedDiff = Effect.fnUntraced(function* ( oldPath: string, newPath: string, oldLabel: string, newLabel: string, -): Promise { - const proc = Bun.spawn(["diff", "-u", "-L", oldLabel, "-L", newLabel, oldPath, newPath], { - stdout: "pipe", - stderr: "pipe", +) { + const proc = yield* Effect.try({ + try: () => + Bun.spawn(["diff", "-u", "-L", oldLabel, "-L", newLabel, oldPath, newPath], { + stdout: "pipe", + stderr: "pipe", + }), + catch: foreignFailure, + }); + const [, stdout] = yield* Effect.tryPromise({ + try: () => Promise.all([proc.exited, new Response(proc.stdout).text()]), + catch: foreignFailure, }); - const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]); return stdout; -} +}); export interface FileEntry { readonly status: "added" | "removed" | "changed"; @@ -69,14 +86,13 @@ export interface CompareResult { } /** Diffs two emitted `.d.ts` trees (`**\/*.d.ts` only — the glob itself never matches `.d.ts.map`). */ -export async function diffDeclarationTrees( - headDir: string, - baseDir: string, -): Promise { - const [headFiles, baseFiles] = await Promise.all([ - listDeclarationFiles(headDir), - listDeclarationFiles(baseDir), - ]); +export const diffDeclarationTrees = Effect.fnUntraced(function* (headDir: string, baseDir: string) { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const [headFiles, baseFiles] = yield* Effect.all( + [listDeclarationFiles(fs, headDir), listDeclarationFiles(fs, baseDir)], + { concurrency: "unbounded" }, + ); const headSet = new Set(headFiles); const baseSet = new Set(baseFiles); @@ -87,7 +103,7 @@ export async function diffDeclarationTrees( entries.push({ status: "added", path: relativePath, - diff: await unifiedDiff( + diff: yield* unifiedDiff( "/dev/null", path.join(headDir, relativePath), "/dev/null", @@ -97,15 +113,18 @@ export async function diffDeclarationTrees( continue; } - const [headContent, baseContent] = await Promise.all([ - readFile(path.join(headDir, relativePath), "utf8"), - readFile(path.join(baseDir, relativePath), "utf8"), - ]); + const [headContent, baseContent] = yield* Effect.all( + [ + readUtf8(fs, path.join(headDir, relativePath)), + readUtf8(fs, path.join(baseDir, relativePath)), + ], + { concurrency: "unbounded" }, + ); if (headContent !== baseContent) { entries.push({ status: "changed", path: relativePath, - diff: await unifiedDiff( + diff: yield* unifiedDiff( path.join(baseDir, relativePath), path.join(headDir, relativePath), `base/${relativePath}`, @@ -120,7 +139,7 @@ export async function diffDeclarationTrees( entries.push({ status: "removed", path: relativePath, - diff: await unifiedDiff( + diff: yield* unifiedDiff( path.join(baseDir, relativePath), "/dev/null", `base/${relativePath}`, @@ -132,7 +151,7 @@ export async function diffDeclarationTrees( entries.sort((a, b) => a.path.localeCompare(b.path)); return { identical: entries.length === 0, entries }; -} +}); export function countByStatus(entries: readonly FileEntry[], status: FileEntry["status"]): number { return entries.filter((entry) => entry.status === status).length; @@ -197,10 +216,11 @@ export function renderDiffDetailsBlocks(entries: readonly FileEntry[]): string[] return lines; } -export async function writeStepSummary(markdown: string): Promise { - const summaryPath = process.env.GITHUB_STEP_SUMMARY; +export const writeStepSummary = Effect.fnUntraced(function* (markdown: string) { + const summaryPath = yield* Config.String("GITHUB_STEP_SUMMARY").pipe(Config.withDefault("")); if (!summaryPath) { return; } - await appendFile(summaryPath, `${markdown}\n`); -} + const fs = yield* FileSystem.FileSystem; + yield* fs.writeFileString(summaryPath, `${markdown}\n`, { flag: "a" }); +}); diff --git a/tools/release/local-registry.ts b/tools/release/local-registry.ts index 9520dcf7cb..5d83e7fab3 100644 --- a/tools/release/local-registry.ts +++ b/tools/release/local-registry.ts @@ -12,114 +12,167 @@ * crash or hard kill cannot leave the developer's machine pointed at localhost. */ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; +import { Console, Effect, FileSystem, Layer, Path, Schema } from "effect"; +import { FetchHttpClient, HttpClient } from "effect/http"; +// oxlint-disable-next-line effecttsgo/node-builtin-import -- Effect ChildProcess cannot hand Verdaccio one log descriptor shared by stdout and stderr. import { openSync } from "node:fs"; -import { mkdir, rm, writeFile } from "node:fs/promises"; -import path from "node:path"; const PORT = 4873; const REGISTRY = `http://localhost:${PORT}`; const LOCAL_USER = "local-dev"; const LOCAL_PASS = "local-dev-password"; -const root = path.resolve(import.meta.dir, "../.."); -const tmpDir = path.join(root, "tmp", "verdaccio"); -const configPath = path.join(root, "tmp", "verdaccio.yaml"); -const tokenPath = path.join(root, "tmp", "verdaccio-token"); -const logPath = path.join(root, "tmp", "verdaccio.log"); - -async function isPortInUse(): Promise { - try { - const res = await fetch(`${REGISTRY}/-/ping`, { - signal: AbortSignal.timeout(1000), - }); - return res.ok; - } catch { - return false; - } -} - -async function waitForRegistry(maxAttempts = 30, intervalMs = 500): Promise { +class LocalRegistryError extends Schema.TaggedError()("LocalRegistryError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} + +/* Only `AbortSignal.timeout`, so the deadline alone bounds the request and its unread response body. */ +const abortAfter = (ms: number) => + Effect.updateService(FetchHttpClient.Fetch, (fetch) => + Object.assign( + (input: string | URL | Request, init?: RequestInit) => + fetch(input, { ...init, signal: AbortSignal.timeout(ms) }), + { preconnect: fetch.preconnect }, + ), + ); + +const isPortInUse = Effect.fnUntraced(function* () { + const client = yield* HttpClient.HttpClient; + return yield* client.get(`${REGISTRY}/-/ping`).pipe( + abortAfter(1000), + Effect.map((res) => res.status >= 200 && res.status < 300), + Effect.orElseSucceed(() => false), + ); +}); + +const waitForRegistry = Effect.fnUntraced(function* ( + logPath: string, + maxAttempts = 30, + intervalMs = 500, +) { + const client = yield* HttpClient.HttpClient; for (let i = 0; i < maxAttempts; i++) { - try { - const res = await fetch(`${REGISTRY}/-/ping`, { - signal: AbortSignal.timeout(1000), - }); - if (res.ok) return; - } catch { + const ready = yield* client.get(`${REGISTRY}/-/ping`).pipe( + abortAfter(1000), + Effect.map((res) => res.status >= 200 && res.status < 300), // not ready yet - } - await Bun.sleep(intervalMs); + Effect.orElseSucceed(() => false), + ); + if (ready) return; + yield* Effect.sleep(intervalMs); } const totalSeconds = (maxAttempts * intervalMs) / 1000; - throw new Error(`Verdaccio did not become ready within ${totalSeconds}s. Check logs: ${logPath}`); -} - -async function createUser(): Promise { - const res = await fetch(`${REGISTRY}/-/user/org.couchdb.user:${LOCAL_USER}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: LOCAL_USER, - password: LOCAL_PASS, - email: "local@local.dev", - _id: `org.couchdb.user:${LOCAL_USER}`, - type: "user", - roles: [], - }), + return yield* new LocalRegistryError({ + message: `Verdaccio did not become ready within ${totalSeconds}s. Check logs: ${logPath}`, }); +}); + +const foreignFailure = (cause: unknown) => + new LocalRegistryError({ + message: cause instanceof Error ? cause.message : String(cause), + cause, + }); + +const tryAsync = (evaluate: () => PromiseLike) => + Effect.tryPromise({ try: evaluate, catch: foreignFailure }); + +/* Native `fetch` and `Response.json`, whose empty and null body handling `HttpClientResponse` cannot reproduce. */ +const createUser = Effect.fnUntraced(function* () { + const fetch = yield* FetchHttpClient.Fetch; + const res = yield* tryAsync(() => + fetch(`${REGISTRY}/-/user/org.couchdb.user:${LOCAL_USER}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: LOCAL_USER, + password: LOCAL_PASS, + email: "local@local.dev", + _id: `org.couchdb.user:${LOCAL_USER}`, + type: "user", + roles: [], + }), + }), + ); if (!res.ok) { - const body = await res.text(); - throw new Error(`Failed to create Verdaccio user (HTTP ${res.status}): ${body}`); + const body = yield* tryAsync(() => res.text()); + return yield* new LocalRegistryError({ + message: `Failed to create Verdaccio user (HTTP ${res.status}): ${body}`, + }); } - const body = (await res.json()) as { token?: string }; + const body = (yield* tryAsync(() => res.json())) as { token?: string }; if (!body.token) { - throw new Error("Verdaccio did not return an auth token after user creation."); + return yield* new LocalRegistryError({ + message: "Verdaccio did not return an auth token after user creation.", + }); } return body.token; -} +}); + +const main = Effect.fnUntraced(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; -async function main() { - if (await isPortInUse()) { - console.error(`\nError: Something is already running on port ${PORT}.`); - console.error("If it's a leftover Verdaccio process, kill it first and retry.\n"); + const root = path.resolve(import.meta.dir, "../.."); + const tmpDir = path.join(root, "tmp", "verdaccio"); + const configPath = path.join(root, "tmp", "verdaccio.yaml"); + const tokenPath = path.join(root, "tmp", "verdaccio-token"); + const logPath = path.join(root, "tmp", "verdaccio.log"); + + if (yield* isPortInUse()) { + yield* Console.error(`\nError: Something is already running on port ${PORT}.`); + yield* Console.error("If it's a leftover Verdaccio process, kill it first and retry.\n"); process.exit(1); } // Fresh storage each run to avoid stale auth or package conflicts. - await rm(tmpDir, { recursive: true, force: true }); - await mkdir(tmpDir, { recursive: true }); + yield* fs.remove(tmpDir, { recursive: true, force: true }); + yield* fs.makeDirectory(tmpDir, { recursive: true }); - const template = await Bun.file(path.join(root, "verdaccio.yaml")).text(); + const template = yield* tryAsync(() => Bun.file(path.join(root, "verdaccio.yaml")).text()); const resolved = template.replaceAll("{root}", root); - await writeFile(configPath, resolved, "utf-8"); + yield* fs.writeFileString(configPath, resolved); - const logFd = openSync(logPath, "w"); + const logFd = yield* Effect.try({ + try: () => openSync(logPath, "w"), + catch: foreignFailure, + }); const verdaccioBin = path.join(root, "node_modules", ".bin", "verdaccio"); - const proc = Bun.spawn([verdaccioBin, "--config", configPath], { - cwd: root, - stdout: logFd, - stderr: logFd, + const proc = yield* Effect.try({ + try: () => + Bun.spawn([verdaccioBin, "--config", configPath], { + cwd: root, + stdout: logFd, + stderr: logFd, + }), + catch: foreignFailure, }); // Handle unexpected Verdaccio crash. - void proc.exited.then((code) => { - if (code !== 0 && code !== null) { - console.error(`\nVerdaccio exited unexpectedly (code ${code}). Check logs: ${logPath}\n`); - process.exit(1); - } - }); + yield* Effect.forkChild( + Effect.gen(function* () { + const code = yield* Effect.promise(() => proc.exited); + if (code !== 0 && code !== null) { + yield* Console.error( + `\nVerdaccio exited unexpectedly (code ${code}). Check logs: ${logPath}\n`, + ); + process.exit(1); + } + }), + ); process.stdout.write(`Starting local registry at ${REGISTRY}...`); - await waitForRegistry(); + yield* waitForRegistry(logPath); process.stdout.write(" ready.\n"); // Persisted so local-release.ts can read it. - const token = await createUser(); - await writeFile(tokenPath, token, "utf-8"); + const token = yield* createUser(); + yield* fs.writeFileString(tokenPath, token); - console.log(` + yield* Console.log(` Registry : ${REGISTRY} Token : ${tokenPath} Logs : ${logPath} @@ -144,7 +197,23 @@ async function main() { process.on("SIGHUP", cleanup); // Block until a signal is received. - await new Promise(() => {}); -} - -await main(); + return yield* Effect.never; +}); + +await Effect.runPromise( + main().pipe( + /* Dies with the step's own cause, or a plain Error with its message, so the uncaught report has no `_tag` line. */ + Effect.catchTags({ + LocalRegistryError: (error) => Effect.die(error.cause ?? new Error(error.message)), + PlatformError: (error) => Effect.die(error.cause ?? error), + }), + Effect.provide( + Layer.mergeAll( + BunFileSystem.layer, + BunPath.layer, + FetchHttpClient.layer, + Layer.succeed(HttpClient.TracerPropagationEnabled, false), + ), + ), + ), +); diff --git a/tools/release/local-release.ts b/tools/release/local-release.ts index 370ba72188..c22fc3b8e9 100644 --- a/tools/release/local-release.ts +++ b/tools/release/local-release.ts @@ -10,18 +10,27 @@ * Requires Go in PATH to build the `supabase-go` sidecar. */ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -import { mkdir, mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { Clock, Console, Effect, FileSystem, Layer, Path, Schema } from "effect"; +import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/http"; import process from "node:process"; import { parseArgs } from "node:util"; import { compileOptions, stackReleaseDefine } from "../../apps/cli/scripts/compile-options.ts"; const PORT = 4873; const REGISTRY = `http://localhost:${PORT}`; -const root = path.resolve(import.meta.dir, "../.."); -const tokenPath = path.join(root, "tmp", "verdaccio-token"); + +class LocalReleaseError extends Schema.TaggedError()("LocalReleaseError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} + +const foreignFailure = (cause: unknown) => + new LocalReleaseError({ message: cause instanceof Error ? cause.message : String(cause), cause }); + +const tryAsync = (evaluate: () => PromiseLike) => + Effect.tryPromise({ try: evaluate, catch: foreignFailure }); // Every platform package that appears in optionalDependencies. const PLATFORM_PACKAGES = [ @@ -88,144 +97,184 @@ const PLATFORM_MAP: Record = { }, }; -function getPlatformInfo(): PlatformInfo { +const getPlatformInfo = Effect.fnUntraced(function* () { const key = `${process.platform}-${process.arch}`; const info = PLATFORM_MAP[key]; if (!info) { - console.error(`\nError: Unsupported platform: ${key}`); - console.error("Supported: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64\n"); + yield* Console.error(`\nError: Unsupported platform: ${key}`); + yield* Console.error( + "Supported: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64\n", + ); process.exit(1); } return info; -} - -async function checkRegistry(): Promise { - try { - const res = await fetch(`${REGISTRY}/-/ping`, { - signal: AbortSignal.timeout(3000), - }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - } catch { - console.error(`\nError: Local registry not responding at ${REGISTRY}`); - console.error("Start it first with: pnpm local-registry\n"); - process.exit(1); - } -} - -async function readToken(): Promise { - try { - return (await Bun.file(tokenPath).text()).trim(); - } catch { - console.error(`\nError: Auth token not found at ${tokenPath}`); - console.error("The local registry must be running before you release: pnpm local-registry\n"); - process.exit(1); - } -} - -async function checkGo(): Promise { - try { - await $`go version`.quiet(); - } catch { - console.error("\nError: `go` not found in PATH."); - console.error("Install Go from https://go.dev/dl/ to build the CLI.\n"); - process.exit(1); - } -} - -async function checkGoSource(): Promise { +}); + +/* Only `AbortSignal.timeout`, so the deadline alone bounds the request and its unread response body. */ +const abortAfter = (ms: number) => + Effect.updateService(FetchHttpClient.Fetch, (fetch) => + Object.assign( + (input: string | URL | Request, init?: RequestInit) => + fetch(input, { ...init, signal: AbortSignal.timeout(ms) }), + { preconnect: fetch.preconnect }, + ), + ); + +const checkRegistry = Effect.fnUntraced(function* () { + const client = yield* HttpClient.HttpClient; + yield* client.get(`${REGISTRY}/-/ping`).pipe( + abortAfter(3000), + Effect.flatMap(HttpClientResponse.filterStatusOk), + Effect.catch(() => + Effect.gen(function* () { + yield* Console.error(`\nError: Local registry not responding at ${REGISTRY}`); + yield* Console.error("Start it first with: pnpm local-registry\n"); + return process.exit(1); + }), + ), + ); +}); + +const readToken = Effect.fnUntraced(function* (tokenPath: string) { + return yield* tryAsync(() => Bun.file(tokenPath).text()).pipe( + Effect.map((token) => token.trim()), + Effect.catch(() => + Effect.gen(function* () { + yield* Console.error(`\nError: Auth token not found at ${tokenPath}`); + yield* Console.error( + "The local registry must be running before you release: pnpm local-registry\n", + ); + return process.exit(1); + }), + ), + ); +}); + +const checkGo = Effect.fnUntraced(function* () { + yield* tryAsync(() => $`go version`.quiet()).pipe( + Effect.catch(() => + Effect.gen(function* () { + yield* Console.error("\nError: `go` not found in PATH."); + yield* Console.error("Install Go from https://go.dev/dl/ to build the CLI.\n"); + return process.exit(1); + }), + ), + ); +}); + +const checkGoSource = Effect.fnUntraced(function* (path: Path.Path, root: string) { const goSource = path.join(root, "apps", "cli-go"); const goMod = Bun.file(path.join(goSource, "go.mod")); - if (!(await goMod.exists())) { - console.error("\nError: Go CLI source not found at apps/cli-go"); - console.error("Run: pnpm repos:install\n"); + if (!(yield* tryAsync(() => goMod.exists()))) { + yield* Console.error("\nError: Go CLI source not found at apps/cli-go"); + yield* Console.error("Run: pnpm repos:install\n"); process.exit(1); } return goSource; -} - -async function main() { - const { values } = parseArgs({ - options: { - version: { type: "string" }, - }, +}); + +const main = Effect.fnUntraced(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = path.resolve(import.meta.dir, "../.."); + const tokenPath = path.join(root, "tmp", "verdaccio-token"); + + const { values } = yield* Effect.try({ + try: () => + parseArgs({ + options: { + version: { type: "string" }, + }, + }), + catch: foreignFailure, }); - const version = values.version ?? `0.0.0-local.${Math.floor(Date.now() / 1000)}`; + const version = + values.version ?? `0.0.0-local.${Math.floor((yield* Clock.currentTimeMillis) / 1000)}`; - await checkRegistry(); - const token = await readToken(); - const platform = getPlatformInfo(); + yield* checkRegistry(); + const token = yield* readToken(tokenPath); + const platform = yield* getPlatformInfo(); - await checkGo(); - const goSource = await checkGoSource(); + yield* checkGo(); + const goSource = yield* checkGoSource(path, root); if (process.platform === "linux") { - console.warn( + yield* Console.warn( "Note: local-release builds the glibc variant only (cli-linux-*). " + "The musl variant is skipped for local dev.\n", ); } - const tmpDir = await mkdtemp(path.join(tmpdir(), "supabase-local-release-")); + const tmpDir = yield* fs.makeTempDirectory({ prefix: "supabase-local-release-" }); - try { + const exit = yield* Effect.gen(function* () { // Read once up front so log lines and the published package.json agree. - const cliPkgJson = await Bun.file(path.join(root, "apps", "cli", "package.json")).json(); + const cliPkgJson = yield* tryAsync(() => + Bun.file(path.join(root, "apps", "cli", "package.json")).json(), + ); const umbrellaName: string = cliPkgJson.name; - console.log(`\nBuilding ${umbrellaName}@${version} (${platform.platformPkg})...\n`); + yield* Console.log(`\nBuilding ${umbrellaName}@${version} (${platform.platformPkg})...\n`); const tmpPlatformDir = path.join(tmpDir, platform.platformPkg); const tmpPlatformBinDir = path.join(tmpPlatformDir, "bin"); - await mkdir(tmpPlatformBinDir, { recursive: true }); + yield* fs.makeDirectory(tmpPlatformBinDir, { recursive: true }); const entrypoint = path.join(root, "apps", "cli", "src", "main.ts"); const bunBinary = path.join(tmpPlatformBinDir, `supabase${platform.ext}`); - console.log("[1/3] Compiling CLI binary..."); - const buildResult = await Bun.build({ - entrypoints: [entrypoint], - compile: { target: platform.bunTarget, outfile: bunBinary }, - ...compileOptions, - define: await stackReleaseDefine(), - }); + yield* Console.log("[1/3] Compiling CLI binary..."); + const define = yield* tryAsync(() => stackReleaseDefine()); + const buildResult = yield* tryAsync(() => + Bun.build({ + entrypoints: [entrypoint], + compile: { target: platform.bunTarget, outfile: bunBinary }, + ...compileOptions, + define, + }), + ); for (const log of buildResult.logs) { - console.warn(log); + yield* Console.warn(log); } if (!buildResult.success) { - throw new Error("failed to compile the CLI binary"); + return yield* new LocalReleaseError({ message: "failed to compile the CLI binary" }); } { const goBinary = path.join(tmpPlatformBinDir, `supabase-go${platform.ext}`); - console.log(`[2/3] Compiling Go CLI binary (${platform.goos}/${platform.goarch})...`); + yield* Console.log(`[2/3] Compiling Go CLI binary (${platform.goos}/${platform.goarch})...`); // go build must run from the Go source directory: passing an absolute path as a positional // arg makes Go resolve the module from CWD instead, which fails because the repo root has // no go.mod. - await $`go build -trimpath -ldflags="-s -w" -o ${goBinary} .`.cwd(goSource).env({ - ...process.env, - GOOS: platform.goos, - GOARCH: platform.goarch, - CGO_ENABLED: "0", - }); + yield* tryAsync(() => + $`go build -trimpath -ldflags="-s -w" -o ${goBinary} .`.cwd(goSource).env({ + ...process.env, + GOOS: platform.goos, + GOARCH: platform.goarch, + CGO_ENABLED: "0", + }), + ); } const tmpCliDir = path.join(tmpDir, "cli"); const tmpCliDistDir = path.join(tmpCliDir, "dist"); - await mkdir(tmpCliDistDir, { recursive: true }); + yield* fs.makeDirectory(tmpCliDistDir, { recursive: true }); const shimSrc = path.join(root, "apps", "cli", "src", "shared", "cli", "bin.ts"); const shimOut = path.join(tmpCliDistDir, "supabase.js"); - console.log("[3/3] Building Node.js shim..."); - await $`bun build ${shimSrc} --outfile=${shimOut} --target=node`; + yield* Console.log("[3/3] Building Node.js shim..."); + yield* tryAsync(() => $`bun build ${shimSrc} --outfile=${shimOut} --target=node`); - const platformPkgJson = await Bun.file( - path.join(root, "packages", platform.platformPkg, "package.json"), - ).json(); + const platformPkgJson = yield* tryAsync(() => + Bun.file(path.join(root, "packages", platform.platformPkg, "package.json")).json(), + ); platformPkgJson.version = version; - await Bun.write( - path.join(tmpPlatformDir, "package.json"), - `${JSON.stringify(platformPkgJson, null, "\t")}\n`, + yield* tryAsync(() => + Bun.write( + path.join(tmpPlatformDir, "package.json"), + `${JSON.stringify(platformPkgJson, null, "\t")}\n`, + ), ); // The shim only uses Node built-ins; @supabase/* and catalog: deps are bundled in the platform @@ -244,31 +293,39 @@ async function main() { publishConfig: cliPkgJson.publishConfig, optionalDependencies: resolvedOptionalDeps, }; - await Bun.write( - path.join(tmpCliDir, "package.json"), - `${JSON.stringify(publishPkgJson, null, "\t")}\n`, + yield* tryAsync(() => + Bun.write( + path.join(tmpCliDir, "package.json"), + `${JSON.stringify(publishPkgJson, null, "\t")}\n`, + ), ); const npmrc = [`registry=${REGISTRY}`, `//localhost:${PORT}/:_authToken=${token}`, ""].join( "\n", ); - await Bun.write(path.join(tmpPlatformDir, ".npmrc"), npmrc); - await Bun.write(path.join(tmpCliDir, ".npmrc"), npmrc); + yield* tryAsync(() => Bun.write(path.join(tmpPlatformDir, ".npmrc"), npmrc)); + yield* tryAsync(() => Bun.write(path.join(tmpCliDir, ".npmrc"), npmrc)); - console.log(`\nPublishing @supabase/${platform.platformPkg}@${version} to local registry...`); + yield* Console.log( + `\nPublishing @supabase/${platform.platformPkg}@${version} to local registry...`, + ); // bun publish (not pnpm) for the platform binary package: pnpm normalizes tarball file modes // and strips the execute bit from files outside the package's `bin` field; bun publish // preserves modes, matching production. - await $`bun publish --access public --tag local --registry ${REGISTRY} --no-git-checks`.cwd( - tmpPlatformDir, + yield* tryAsync(() => + $`bun publish --access public --tag local --registry ${REGISTRY} --no-git-checks`.cwd( + tmpPlatformDir, + ), ); - console.log(`Publishing ${umbrellaName}@${version} to local registry...`); - await $`pnpm publish --access public --tag local --registry ${REGISTRY} --no-git-checks`.cwd( - tmpCliDir, + yield* Console.log(`Publishing ${umbrellaName}@${version} to local registry...`); + yield* tryAsync(() => + $`pnpm publish --access public --tag local --registry ${REGISTRY} --no-git-checks`.cwd( + tmpCliDir, + ), ); - console.log(` + yield* Console.log(` ✓ Published ${umbrellaName}@${version} Test with npx: @@ -278,9 +335,25 @@ Or install globally: npm install -g --registry ${REGISTRY} ${umbrellaName}@${version} supabase --version `); - } finally { - await rm(tmpDir, { recursive: true, force: true }); - } -} - -await main(); + }).pipe(Effect.exit); + yield* fs.remove(tmpDir, { recursive: true, force: true }); + return yield* exit; +}); + +await Effect.runPromise( + main().pipe( + /* Dies with the step's own cause, or a plain Error with its message, so the uncaught report has no `_tag` line. */ + Effect.catchTags({ + LocalReleaseError: (error) => Effect.die(error.cause ?? new Error(error.message)), + PlatformError: (error) => Effect.die(error.cause ?? error), + }), + Effect.provide( + Layer.mergeAll( + BunFileSystem.layer, + BunPath.layer, + FetchHttpClient.layer, + Layer.succeed(HttpClient.TracerPropagationEnabled, false), + ), + ), + ), +);