diff --git a/.oxlintrc.effect.json b/.oxlintrc.effect.json index 9a7f7befa9..9712e56950 100644 --- a/.oxlintrc.effect.json +++ b/.oxlintrc.effect.json @@ -6,135 +6,27 @@ }, "ignorePatterns": [ "**", + "!packages/api/**", "!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/**", + "!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", "apps/cli/src/shared/compute/stacks/**", "apps/cli/src/shared/feedback/database.types.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/argv-flag-reconcile.*", - "!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/byte-quote.ts", - "!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/duration.*", - "!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/format-float.*", - "!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/hostname.*", - "!apps/cli/src/command-internal/html-safe-json.*", - "!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-jwt.*", - "!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/output-formats.*", - "!apps/cli/src/command-internal/output.encoders.*", - "!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/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/struct-output.*", - "!apps/cli/src/command-internal/supabase-env.*", - "!apps/cli/src/command-internal/target/parent-scoped-project-ref.*", - "!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/workdir-project.*", - "!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/src/command-internal/db-bootstrap/local-db-running.ts", + "apps/cli/src/command-internal/config-validate.parity.unit.test.ts", + "apps/cli/src/command-internal/hosted-consent/hosted-consent-coverage.unit.test.ts", + "apps/cli/tests/helpers/config-goldens.ts", + "apps/cli/tests/helpers/config-values-layer.integration.test.ts", + "apps/cli/tests/helpers/db-target-selection.ts", + "apps/cli/tests/helpers/hosted-consent.ts", + "apps/cli/tests/helpers/hosted-target-selection.ts", + "packages/api/src/generated/contracts.ts" ] } 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. 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/cli/AGENTS.md b/apps/cli/AGENTS.md index 67fce5b234..d8d6ff4ab2 100644 --- a/apps/cli/AGENTS.md +++ b/apps/cli/AGENTS.md @@ -229,6 +229,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. + ## Targets and hosted consent Every leaf declares its target group and consent default with `withTargetPolicy`; the 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 4fea17c50f..380c5c3655 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 { 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,306 +29,356 @@ 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.`, - ); -} +class BuildStepFailed extends Data.TaggedError("BuildStepFailed")<{ readonly cause: unknown }> {} -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 step = (run: () => PromiseLike) => + Effect.tryPromise({ try: run, catch: (cause) => new BuildStepFailed({ cause }) }); -const entrypoint = path.join(root, "apps/cli/src/main.ts"); -const distDir = path.join(root, "dist"); -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 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); - } -} +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); -async function buildTarget(target: (typeof TARGETS)[number]) { - const binDir = path.join(root, "packages", target.pkg, "bin"); - await mkdir(binDir, { recursive: true }); +/* Native `JSON.parse`, so a malformed package.json fails with its own `SyntaxError`. */ +const parsePackageJson = (text: string) => JSON.parse(text) as { version?: string }; - const outfile = path.join(binDir, `supabase${target.ext}`); +const envOrEmpty = (name: string) => Config.String(name).pipe(Config.withDefault("")); - 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.`); -} +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))))); -/** - * 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"; - } - 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).", - ); + 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 }), + }); + const version = values.version ?? packageVersion.version; + if (!version) { + yield* Console.error("Usage: bun apps/cli/scripts/build.ts [--version ]"); 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; + if (values.version === undefined) { + yield* Console.warn( + `[build] --version not provided; falling back to package.json version "${version}". Pass --version explicitly in release builds.`, + ); } - const darwinTargets = TARGETS.filter((target) => target.bunTarget.startsWith("bun-darwin")); - const binaries = darwinBinaries(); + 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 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 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); + } + }); - for (const target of darwinTargets) { + 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 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's linker-signed one. - await $`rcodesign sign --binary-identifier ${identifier} ${binPath}`; + /** + * 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"; + }); - 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 signDarwinBinaries = Effect.fnUntraced(function* (mode: SignMode) { + if (mode === "off") { + return; } - } -} -async function archiveTarget(target: (typeof TARGETS)[number]) { - const binDir = path.join(root, "packages", target.pkg, "bin"); - const archivePath = path.join(distDir, target.archive); - - console.log(`[${target.pkg}] Creating archive ${target.archive}...`); - - if (target.archive.endsWith(".zip")) { - const files = [path.join(binDir, `supabase${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}`]; - console.log(`[${target.pkg}] Creating archive ${tarArchive}...`); - await $`tar -czf ${tarArchivePath} -C ${binDir} ${tarFiles}`; - } else { - const files = [`supabase${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), - }, - }); - - console.log(`[${target.pkg}] Done.`); - }), - ); -} -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; - - const contents: Array<{ src: string; dst: string }> = [ - { src: path.join(binDir, "supabase"), dst: "/usr/bin/supabase" }, - ]; - - 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"]; + 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's linker-signed one. + yield* step(() => $`rcodesign sign --binary-identifier ${identifier} ${binPath}`); + + 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 configPath = path.join(distDir, `nfpm-${target.nfpmArch}-${fmt}.yaml`); - await writeFile(configPath, JSON.stringify(nfpmConfig)); - - jobs.push( - (async () => { - console.log(`[${target.pkg}] Creating ${outFile}...`); - await $`nfpm package --config ${configPath} --packager ${fmt} --target ${outPath}`; - await rm(configPath); - })(), - ); } - } - - await Promise.all(jobs); -} - -async function generateChecksums() { - const lines: string[] = []; + }); - for (const target of TARGETS) { + 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); - const data = await readFile(archivePath); - const hash = createHash("sha256").update(data).digest("hex"); - lines.push(`${hash} ${target.archive}`); + + yield* Console.log(`[${target.pkg}] Creating archive ${target.archive}...`); if (target.archive.endsWith(".zip")) { + const files = [path.join(binDir, `supabase${target.ext}`)]; + yield* step(() => $`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 tarData = await readFile(path.join(distDir, tarArchive)); - const tarHash = createHash("sha256").update(tarData).digest("hex"); - lines.push(`${tarHash} ${tarArchive}`); + const tarArchivePath = path.join(distDir, tarArchive); + const tarFiles = [`supabase${target.ext}`]; + yield* Console.log(`[${target.pkg}] Creating archive ${tarArchive}...`); + yield* step(() => $`tar -czf ${tarArchivePath} -C ${binDir} ${tarFiles}`); + } else { + const files = [`supabase${target.ext}`]; + yield* step(() => $`tar -czf ${archivePath} -C ${binDir} ${files}`); } - } + }); - 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 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), + }, + }); + + yield* Console.log(`[${target.pkg}] Done.`); + }), + ), + ), + ), + ); + }); + + 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; + + const contents: Array<{ src: string; dst: string }> = [ + { src: path.join(binDir, "supabase"), dst: "/usr/bin/supabase" }, + ]; + + 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); + }), + ), + ); + } + } + + yield* step(() => Promise.all(jobs)); + }); + + const generateChecksums = Effect.fnUntraced(function* () { + const lines: string[] = []; + + 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} ${filename}`); + lines.push(`${hash} ${target.archive}`); + + 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 checksumsPath = path.join(distDir, "checksums.txt"); - await writeFile(checksumsPath, `${lines.join("\n")}\n`); - console.log("Checksums written to dist/checksums.txt"); -} + 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"); + yield* fs.writeFileString(checksumsPath, `${lines.join("\n")}\n`); + yield* Console.log("Checksums written to dist/checksums.txt"); + }); -console.log(`Building the CLI for ${TARGETS.length} targets...\n`); + yield* Console.log(`Building the CLI for ${TARGETS.length} targets...\n`); -await Promise.all(TARGETS.map(buildTarget)); + yield* step(() => Promise.all(TARGETS.map((target) => runJob(buildTarget(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); + // 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); -await mkdir(distDir, { recursive: true }); -await Promise.all(TARGETS.map(archiveTarget)); + yield* fs.makeDirectory(distDir, { recursive: true }); + yield* step(() => Promise.all(TARGETS.map((target) => runJob(archiveTarget(target))))); -await buildMuslBinaries(); -await buildLinuxPackages(version); -await generateChecksums(); + yield* buildMuslBinaries(); + yield* buildLinuxPackages(version); + yield* generateChecksums(); -console.log("\nAll targets built successfully."); + yield* Console.log("\nAll targets built successfully."); +}); + +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/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 36cde0ec1f..e853a57189 100644 --- a/apps/cli/scripts/render-service-dockerfile.ts +++ b/apps/cli/scripts/render-service-dockerfile.ts @@ -15,6 +15,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. @@ -156,30 +157,43 @@ 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) { 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.", ); - process.exit(1); + return yield* Effect.sync(() => process.exit(1)); } - console.log("The Dockerfile matches the catalog."); + yield* Console.log("The Dockerfile matches the catalog."); return; } - await Bun.write(TS_DOCKERFILE_PATH, rendered); - console.log(`Regenerated ${TS_DOCKERFILE_PATH} from the catalog.`); -} + yield* step(() => Bun.write(TS_DOCKERFILE_PATH, rendered)); + yield* Console.log(`Regenerated ${TS_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 41e45bb9ed..b7ec382d00 100644 --- a/apps/cli/scripts/render-service-dockerfile.unit.test.ts +++ b/apps/cli/scripts/render-service-dockerfile.unit.test.ts @@ -1,11 +1,16 @@ -import { readFileSync } from "node:fs"; import { describe, expect, test } from "vitest"; import { TS_DOCKERFILE_PATH, 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)", () => { 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 86e8d08a61..14744fe66b 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,51 +16,63 @@ 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"); +class FormulaError extends Data.TaggedError("FormulaError")<{ readonly message: string }> {} -// 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); +const shell = (command: () => PromiseLike) => + Effect.tryPromise({ try: command, catch: (cause) => new ShellFailed({ cause }) }); -const installBlock = [` bin.install "supabase"`].join("\n"); +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 conflictsBlock = homebrewConflictsLine(name); -const conflictsSection = conflictsBlock ? `${conflictsBlock}\n\n` : ""; + 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"); -const testInvocation = `#{bin}/supabase`; + // 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); -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 installBlock = [` bin.install "supabase"`].join("\n"); -function sha(file: string): string { - const hash = checksums.get(file); - if (!hash) throw new Error(`Checksum not found for ${file}`); - return hash; -} + 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}" @@ -70,20 +81,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 @@ -97,43 +108,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", + ]); + }), + ); }); 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 814b4539e6..282aca5eaa 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"; @@ -18,6 +18,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, @@ -80,9 +107,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: [ { @@ -123,7 +153,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) { @@ -132,12 +164,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/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 72012d78a3..ec202899df 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 @@ -16,6 +16,8 @@ import { resolveLocalConfigValues } from "./local-config-values.ts"; import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts"; import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts"; +const stringifyJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); + /** * Cross-caller parity coverage: for a table of shared misconfigurations, drives both real * pipelines — D (`readDbToml`, Effect/raw-TOML) and L (`resolveLocalConfigValues`, @@ -51,7 +53,7 @@ function failsWithD(tomlLines: ReadonlyArray, message: string) { 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(stringifyJson(exit.cause)).toContain(message); } rmSync(dir, { recursive: true, force: true }); }); @@ -70,8 +72,13 @@ function failsWithL( message: string, document?: Readonly>, ) { - const config = baseConfig(overrides); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document)).toThrow(message); + return Effect.gen(function* () { + const config = baseConfig(overrides); + const error = yield* Effect.flip( + resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document), + ); + expect(error.message).toContain(message); + }).pipe(Effect.provide(BunServices.layer)); } interface ParityScenario { @@ -317,7 +324,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); }), ); } @@ -351,7 +358,7 @@ 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(stringifyJson(exit.cause)).toContain(message); } rmSync(dir, { recursive: true, force: true }); }); @@ -374,7 +381,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.port"; - failsWithL({ api: { port: 0 } }, message); + yield* failsWithL({ api: { port: 0 } }, message); yield* failsWithS({ api: { port: 0 } }, message); }), ), @@ -384,7 +391,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({ api: { tls: { enabled: true, cert_path: "kong.crt" } } }, message); }), ), @@ -394,7 +401,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({ auth: { jwt_secret: "short" } }, message); }), ), @@ -402,12 +409,10 @@ describe("shared api + auth validation branches, cross-caller parity (S vs L)", it.effect("undecryptable encrypted: auth.service_role_key fails S at resolvedConfig load", () => isolated( - Effect.gen(function* () { - yield* failsWithS( - { auth: { service_role_key: "encrypted:not-a-real-ciphertext" } }, - "failed to parse config", - ); - }), + failsWithS( + { auth: { service_role_key: "encrypted:not-a-real-ciphertext" } }, + "failed to parse config", + ), ), ); }); diff --git a/apps/cli/src/command-internal/config-validate.ts b/apps/cli/src/command-internal/config-validate.ts index d74c67175f..dbec5fff8b 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 type { CliConfig } from "@supabase/config"; import { @@ -18,8 +17,8 @@ import { parseUrl } 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` needs the raw @@ -49,12 +48,16 @@ export const CLERK_DOMAIN_PATTERN = /^(clerk([.][a-z0-9-]+){2,}|([a-z0-9-]+[.])+clerk[.]accounts[.]dev)$/u; /** - * 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; } @@ -216,6 +219,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 @@ -568,8 +575,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)}` */ @@ -585,7 +598,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` @@ -593,10 +606,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. */ @@ -604,35 +617,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 @@ -643,15 +657,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"` @@ -662,15 +680,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( @@ -684,8 +707,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 3b80e220ea..daf5921a8f 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, @@ -108,33 +99,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", @@ -150,252 +139,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 2578c6a1c5..53952e7881 100644 --- a/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts +++ b/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts @@ -52,26 +52,8 @@ export interface DbBootstrapConfig { readonly storageTargetMigration: string; } -/** - * Wraps a synchronous validation that throws on a malformed value into a typed failure - * instead of an untyped Effect defect. - * - * @param dottedFieldPath - Config path embedded in the error message (`invalid config for - * : `). - */ -function wrapConfigOverride( - dottedFieldPath: string, - thunk: () => T, - 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)}`, - ), - }); -} +const configOverrideMessage = (dottedFieldPath: string, cause: unknown) => + `invalid config for ${dottedFieldPath}: ${cause instanceof Error ? cause.message : String(cause)}`; /** * Resolves every field {@link startDatabase} needs from the effective `config`, ready to feed the @@ -97,22 +79,21 @@ export const resolveDbBootstrapConfig = ( const realtimeEnabledForSetup = config.realtime.enabled; const storageEnabledForSetup = config.storage.enabled; const authEnabledForSetup = config.auth.enabled; - const realtimeIpVersion = yield* wrapConfigOverride( - "realtime.ip_version", - () => narrowConfigEnum("realtime.ip_version", config.realtime.ip_version, ["IPv4", "IPv6"]), - mapConfigError, - ); + const realtimeIpVersion = yield* Effect.try({ + try: () => + narrowConfigEnum("realtime.ip_version", config.realtime.ip_version, ["IPv4", "IPv6"]), + catch: (cause) => mapConfigError(configOverrideMessage("realtime.ip_version", cause)), + }); const realtimeMaxHeaderLength = config.realtime.max_header_length; // `@supabase/config`'s schema stores `file_size_limit` as a plain string without parsing it, // so a malformed value must be validated eagerly here rather than surfacing later inside a // container env builder. const storageFileSizeLimit = config.storage.file_size_limit; - yield* wrapConfigOverride( - "storage.file_size_limit", - () => ramInBytes(storageFileSizeLimit), - mapConfigError, - ); + 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. diff --git a/apps/cli/src/command-internal/db-bootstrap/health-check.ts b/apps/cli/src/command-internal/db-bootstrap/health-check.ts index b9714716fc..eb39284721 100644 --- a/apps/cli/src/command-internal/db-bootstrap/health-check.ts +++ b/apps/cli/src/command-internal/db-bootstrap/health-check.ts @@ -17,8 +17,13 @@ import { } from "../../shared/telemetry/error-actionability.ts"; import { ChildTracePropagation } from "../../shared/telemetry/spans.ts"; import { spawnContainerCliWithRuntime, type ContainerRuntime } from "../container-cli.ts"; +import { yellow } from "../colors.ts"; import { DbConnection, type PgConnInput } from "../db-connection.service.ts"; -import { inspectContainerState } from "../docker-lifecycle.ts"; +import { + formatNotReadyReason, + healthCheckCannotExec, + inspectContainerState, +} from "../docker-lifecycle.ts"; import { kongAuthHeaders } from "../kong-auth.ts"; type Spawner = ChildProcessSpawner["Service"]; @@ -72,6 +77,8 @@ export class HealthCheckTimeoutError extends Data.TaggedError("HealthCheckTimeou * logger is a non-sequitur. */ readonly suggestion?: string; + /** Containers accepted as ready on `running` alone because Docker could not exec their health check. */ + readonly readinessAssumed?: ReadonlyArray; }> { get [ErrorActionabilityId](): CliErrorActionabilityDeclaration { return actionability.startStack; @@ -79,18 +86,31 @@ export class HealthCheckTimeoutError extends Data.TaggedError("HealthCheckTimeou } /** - * PostgREST's local Kong gateway coordinates. TLS/CA trust for a local https gateway is the - * caller's responsibility when composing the `HttpClient.HttpClient` layer this module requires - * — when `api.tls.enabled`, the caller resolves the local Kong CA and overrides the HTTP client - * around the health-check call, so `checkHttpReady` trusts it instead of exhausting the full - * retry budget on a TLS verification failure. + * The local Kong gateway coordinates. TLS/CA trust for a local https gateway is the caller's + * responsibility when composing the `HttpClient.HttpClient` layer this module requires — when + * `api.tls.enabled`, the caller resolves the local Kong CA and overrides the HTTP client around + * the health-check call, so `checkHttpReady` trusts it instead of exhausting the full retry + * budget on a TLS verification failure. */ -export interface HealthCheckPostgrestGateway { - readonly containerId: string; +interface HealthCheckGateway { readonly apiExternalUrl: string; readonly secretKey: string; } +/** A {@link HealthCheckGateway} that probes one specific container instead of its Docker health. */ +export interface HealthCheckPostgrestGateway extends HealthCheckGateway { + readonly containerId: string; +} + +/** + * How to judge a container whose Docker health check cannot run: accept it as `running`, probe + * its route through the gateway, or (for the gateway itself) accept any HTTP response from it. + */ +export type ExecFallbackProbe = + | { readonly kind: "running" } + | { readonly kind: "gateway"; readonly gateway: HealthCheckGateway; readonly path: string } + | { readonly kind: "gateway-reachable"; readonly gateway: HealthCheckGateway }; + export interface WaitForHealthyServicesOptions { readonly timeoutSeconds?: number; readonly postgrest?: HealthCheckPostgrestGateway; @@ -98,6 +118,15 @@ export interface WaitForHealthyServicesOptions { readonly edgeRuntime?: HealthCheckPostgrestGateway; /** Each watched container's already-resolved image, keyed by container name. */ readonly images?: ReadonlyMap; + /** Per-container fallback used only when Docker cannot run that container's health check. */ + readonly execFallbacks?: ReadonlyMap; +} + +export interface WaitForHealthyServicesResult { + /** Containers whose Docker health check could not run, at any point during the wait. */ + readonly execUnavailable: ReadonlyArray; + /** The subset accepted on `running` alone, with no readiness verification. */ + readonly readinessAssumed: ReadonlyArray; } /** The container runtime's message when an image cannot be executed by the host kernel. */ @@ -156,42 +185,99 @@ function execFormatRecoveryHint( ].join("\n"); } +interface ExecFallbackSeen { + readonly execUnavailable: Set; + readonly assumed: Set; +} + /** Not-running/not-ready gating, via the same {@link inspectContainerState} primitive `status.handler.ts` uses. */ -function checkContainerReady(spawner: Spawner, containerId: string): Effect.Effect { +function checkContainerReady( + spawner: Spawner, + containerId: string, + fallback: ExecFallbackProbe | undefined, + seen: ExecFallbackSeen, +): Effect.Effect { return inspectContainerState(spawner, containerId).pipe( Effect.mapError((cause) => cause.message), - Effect.flatMap((state) => { + Effect.flatMap((state): Effect.Effect => { if (!state.running) { return Effect.fail(`container is not running: ${state.status}`); } - if (state.health !== undefined && state.health !== "healthy") { - return Effect.fail(`container is not ready: ${state.health}`); + if (state.health === undefined || state.health === "healthy") { + return Effect.void; } - return Effect.void; + if (healthCheckCannotExec(state)) { + seen.execUnavailable.add(containerId); + if (fallback?.kind === "running") { + seen.assumed.add(containerId); + return Effect.void; + } + if (fallback?.kind === "gateway") { + return checkHttpReady(fallback.gateway, fallback.path, "GET").pipe( + Effect.mapError((reason) => `container is not ready: ${fallback.path}: ${reason}`), + ); + } + if (fallback?.kind === "gateway-reachable") { + return checkHttpReady(fallback.gateway, "/", "GET", true).pipe( + Effect.mapError((reason) => `container is not ready: ${reason}`), + ); + } + } + return Effect.fail(formatNotReadyReason(state)); }), ); } -/** An HTTP HEAD through the local Kong gateway, expecting exactly 200. */ +/** An HTTP request through the local Kong gateway, expecting exactly 200 unless `anyStatus`. */ function checkHttpReady( - gateway: HealthCheckPostgrestGateway, + gateway: HealthCheckGateway, path: string, + method: "HEAD" | "GET" = "HEAD", + anyStatus = false, ): Effect.Effect { return Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; - const request = HttpClientRequest.head(`${gateway.apiExternalUrl}${path}`).pipe( - HttpClientRequest.setHeaders(kongAuthHeaders(gateway.secretKey)), - ); - const response = yield* httpClient.execute(request).pipe( + const url = `${gateway.apiExternalUrl}${path}`; + const request = ( + method === "GET" ? HttpClientRequest.get(url) : HttpClientRequest.head(url) + ).pipe(HttpClientRequest.setHeaders(kongAuthHeaders(gateway.secretKey))); + const status = yield* Effect.gen(function* () { + const response = yield* httpClient.execute(request); + if (method === "GET") { + yield* response.text.pipe(Effect.ignore); + } + return response.status; + }).pipe( Effect.timeout(`${HTTP_PROBE_TIMEOUT_SECONDS} seconds`), Effect.mapError((cause) => String(cause)), ); - if (response.status !== 200) { - return yield* Effect.fail(`unexpected status ${response.status}`); + if (!anyStatus && status !== 200) { + return yield* Effect.fail(`unexpected status ${status}`); } }); } +function execUnavailableHint(containerIds: ReadonlyArray): string | undefined { + const [first] = containerIds; + if (first === undefined) return undefined; + return [ + `Docker could not run the health check inside ${containerIds.join(", ")} ("OCI runtime exec failed"), so it cannot report them healthy.`, + "This happens when Docker runs nested or sandboxed.", + `Check with: docker exec ${first} true`, + ].join("\n"); +} + +/** The stderr warning for a start that proceeded although Docker cannot run health checks here. */ +export function healthCheckExecUnavailableWarning(unverified: ReadonlyArray): string { + return ( + `${yellow("WARNING:")} Docker cannot run health checks inside containers on this host (OCI runtime exec failed). ` + + (unverified.length > 0 + ? `Readiness of ${unverified.join(", ")} was not verified.` + : "Readiness was checked from the host instead.") + + "\n" + ); +} + /** * `docker logs `, teed to this process's stderr. Best-effort: a failure to stream logs must * never mask the timeout error it was printed alongside, so every failure here is swallowed. @@ -263,33 +349,39 @@ function dumpContainerLogs( } /** - * Waits for every container in `containerIds` to become ready. Resolves once all are ready; - * fails with {@link HealthCheckTimeoutError} (carrying every still-unhealthy container's - * last-seen reason) once the retry budget is exhausted. The caller decides whether - * `--ignore-health-check` turns that failure into a warning instead of a hard exit. + * Waits for every container in `containerIds` to become ready. Resolves once all are ready, + * reporting which containers Docker could not health-check; fails with + * {@link HealthCheckTimeoutError} (carrying every still-unhealthy container's last-seen reason) + * once the retry budget is exhausted. The caller decides whether `--ignore-health-check` turns + * that failure into a warning instead of a hard exit. */ export function waitForHealthyServices( spawner: Spawner, containerIds: ReadonlyArray, opts: WaitForHealthyServicesOptions = {}, -): Effect.Effect { +): Effect.Effect { const postgrest = opts.postgrest; const edgeRuntime = opts.edgeRuntime; - const checkOne = (containerId: string): Effect.Effect => { + const checkOne = ( + containerId: string, + seen: ExecFallbackSeen, + ): Effect.Effect => { if (postgrest !== undefined && containerId === postgrest.containerId) { return checkHttpReady(postgrest, POSTGREST_READY_PATH); } if (edgeRuntime !== undefined && containerId === edgeRuntime.containerId) { return checkHttpReady(edgeRuntime, EDGE_RUNTIME_READY_PATH); } - return checkContainerReady(spawner, containerId); + return checkContainerReady(spawner, containerId, opts.execFallbacks?.get(containerId), seen); }; return Effect.gen(function* () { const timeoutSeconds = opts.timeoutSeconds ?? (yield* HealthCheckTimeoutSeconds); let stillWatching = containerIds; + let lastFailures: ReadonlyArray | undefined; let attempts = 0; + const seen: ExecFallbackSeen = { execUnavailable: new Set(), assumed: new Set() }; // Each round narrows `stillWatching` to just the containers that failed, so a container // that becomes healthy mid-run stops being probed on later rounds. @@ -299,7 +391,7 @@ export function waitForHealthyServices( const outcomes = yield* Effect.forEach( stillWatching, (containerId) => - checkOne(containerId).pipe( + checkOne(containerId, seen).pipe( Effect.match({ onFailure: (reason): HealthCheckFailure | undefined => ({ containerId, @@ -315,6 +407,7 @@ export function waitForHealthyServices( ); stillWatching = failures.map((failure) => failure.containerId); if (failures.length > 0) { + lastFailures = failures; return yield* new HealthCheckProbeError({ failures }); } }, @@ -323,8 +416,28 @@ export function waitForHealthyServices( // A 1-second constant delay, capped at `timeoutSeconds` retries after the initial attempt. const schedule = Schedule.max([Schedule.spaced("1 seconds"), Schedule.recurs(timeoutSeconds)]); + const boundSeconds = (timeoutSeconds + 1) * (1 + HTTP_PROBE_TIMEOUT_SECONDS); + yield* probe.pipe( Effect.retry(schedule), + Effect.timeoutOrElse({ + duration: Duration.seconds(boundSeconds), + orElse: () => + Effect.annotateCurrentSpan("health.probe_bound_reached", true).pipe( + Effect.andThen( + Effect.fail( + new HealthCheckProbeError({ + failures: + lastFailures ?? + stillWatching.map((containerId) => ({ + containerId, + reason: `container is not ready: readiness probe hung after ${boundSeconds}s`, + })), + }), + ), + ), + ), + }), Effect.catch((probeError) => Effect.gen(function* () { // An interrupted fiber never reaches this `Effect.catch` handler, so no separate @@ -337,17 +450,25 @@ export function waitForHealthyServices( const execFormatErrors = scans .filter((scan) => scan.found) .map((scan) => scan.containerId); - const suggestion = execFormatRecoveryHint( - execFormatErrors, - opts.images, - scans.find((scan) => scan.runtime !== undefined)?.runtime ?? "docker", - ); + const hints = [ + execFormatRecoveryHint( + execFormatErrors, + opts.images, + scans.find((scan) => scan.runtime !== undefined)?.runtime ?? "docker", + ), + execUnavailableHint( + probeError.failures + .map((failure) => failure.containerId) + .filter((containerId) => seen.execUnavailable.has(containerId)), + ), + ].filter((hint) => hint !== undefined); return yield* new HealthCheckTimeoutError({ message: probeError.failures .map((failure) => `${failure.containerId} ${failure.reason}`) .join("\n"), unhealthy: probeError.failures, - ...(suggestion === undefined ? {} : { suggestion }), + ...(hints.length === 0 ? {} : { suggestion: hints.join("\n\n") }), + ...(seen.assumed.size === 0 ? {} : { readinessAssumed: [...seen.assumed] }), }); }), ), @@ -356,10 +477,16 @@ export function waitForHealthyServices( Effect.annotateCurrentSpan({ "retry.attempt_count": attempts, "health.unhealthy_count": stillWatching.length, + "health.exec_unavailable_count": seen.execUnavailable.size, + "health.readiness_assumed_count": seen.assumed.size, }), ), ), ); + return { + execUnavailable: [...seen.execUnavailable], + readinessAssumed: [...seen.assumed], + }; }).pipe( Effect.withSpan("HealthCheck.waitHealthyServices", { attributes: { "health.container_count": containerIds.length }, @@ -368,18 +495,18 @@ export function waitForHealthyServices( } /** Caps a hung dial so one probe cannot swallow the whole poll budget. */ -const SHADOW_READY_CONNECT_TIMEOUT_SECONDS = 2; +const POSTGRES_READY_CONNECT_TIMEOUT_SECONDS = 2; /** * One round's verdict. `fatal` makes an exited container fail fast instead of burning the * remaining budget: nothing about a dead container can change on a later round. */ -interface ShadowReadyFailure { +interface PostgresReadyFailure { readonly reason: string; readonly fatal: boolean; } -const shadowNotReady = (reason: string): ShadowReadyFailure => ({ +const postgresNotReady = (reason: string): PostgresReadyFailure => ({ reason, fatal: false, }); @@ -390,36 +517,36 @@ const shadowNotReady = (reason: string): ShadowReadyFailure => ({ * the real connection too. `Effect.scoped` closes the session the moment the probe resolves; the * caller opens its own connection afterwards. */ -const probeShadowConnect = ( +const probePostgresConnect = ( connConfig: PgConnInput, -): Effect.Effect => +): Effect.Effect => Effect.scoped( Effect.gen(function* () { const dbConnection = yield* DbConnection; yield* dbConnection.connect( - { ...connConfig, connectTimeoutSeconds: SHADOW_READY_CONNECT_TIMEOUT_SECONDS }, + { ...connConfig, connectTimeoutSeconds: POSTGRES_READY_CONNECT_TIMEOUT_SECONDS }, { isLocal: true, dnsResolver: "native" }, ); }), - ).pipe(Effect.mapError((cause) => shadowNotReady(cause.message))); + ).pipe(Effect.mapError((cause) => postgresNotReady(`container is not ready: ${cause.message}`))); -export interface WaitForShadowReadyOptions { +export interface WaitForPostgresReadyOptions { readonly timeoutSeconds?: number; - /** The shadow container's already-resolved postgres image, named in the exec-format recovery hint. */ + /** The container's already-resolved postgres image, named in the exec-format recovery hint. */ readonly image?: string; } /** - * Shadow readiness: inspect + connect probe, polled every 500ms. Docker's - * healthcheck is `interval=10s` with no start period, so Postgres is connectable - * ~6.5s before `Health.Status` flips. Same error shape as the health gate, - * plus a wall-clock cap so a hung 2s dial cannot overrun the retry budget. + * Postgres readiness: inspect + connect probe, polled every 500ms. Docker's health status needs + * `docker exec`, which some hosts cannot do, and it lags connectability by up to one 10s + * interval. Same error shape as the health gate, plus a wall-clock cap so a hung 2s dial cannot + * overrun the retry budget. */ -export function waitForShadowReady( +export function waitForPostgresReady( spawner: Spawner, containerId: string, connConfig: PgConnInput, - opts: WaitForShadowReadyOptions = {}, + opts: WaitForPostgresReadyOptions = {}, ): Effect.Effect { return Effect.gen(function* () { const timeoutSeconds = opts.timeoutSeconds ?? (yield* HealthCheckTimeoutSeconds); @@ -430,17 +557,17 @@ export function waitForShadowReady( Schedule.spaced("500 millis"), Schedule.recurs(timeoutSeconds * 2), ]); - const boundSeconds = timeoutSeconds + SHADOW_READY_CONNECT_TIMEOUT_SECONDS; + const boundSeconds = timeoutSeconds + POSTGRES_READY_CONNECT_TIMEOUT_SECONDS; // Per-evaluation state: the retry rounds within one evaluation share the latest failure for // the timeout diagnostic, while re-evaluating the returned Effect starts from a fresh slot. - let lastFailure: ShadowReadyFailure | undefined; + let lastFailure: PostgresReadyFailure | undefined; let attempts = 0; - const probe: Effect.Effect = Effect.gen(function* () { + const probe: Effect.Effect = Effect.gen(function* () { attempts += 1; const state = yield* inspectContainerState(spawner, containerId).pipe( - Effect.mapError((cause) => shadowNotReady(cause.message)), + Effect.mapError((cause) => postgresNotReady(cause.message)), ); if (!state.running) { return yield* Effect.fail({ @@ -448,7 +575,7 @@ export function waitForShadowReady( fatal: true, }); } - yield* probeShadowConnect(connConfig); + yield* probePostgresConnect(connConfig); }).pipe( Effect.withTracerEnabled(false), Effect.provideService(ChildTracePropagation, false), @@ -466,7 +593,9 @@ export function waitForShadowReady( orElse: () => Effect.fail( lastFailure ?? - shadowNotReady(`not ready after ${boundSeconds}s: connection attempt hung`), + postgresNotReady( + `container is not ready: connection attempt hung after ${boundSeconds}s`, + ), ), }), Effect.catch((failure) => @@ -491,5 +620,31 @@ export function waitForShadowReady( Effect.suspend(() => Effect.annotateCurrentSpan("retry.attempt_count", attempts)), ), ); - }).pipe(Effect.withSpan("HealthCheck.waitShadowReady")); + }).pipe(Effect.withSpan("HealthCheck.waitPostgresReady")); +} + +/** + * Waits up to `maxSeconds` for Docker's first health verdict on a container, so a caller that + * reads `Health.Status` right after a host-side readiness probe does not see `starting`. + * Best-effort: a timeout or an inspect failure ends the wait without failing. + */ +export function awaitDockerHealthSettled( + spawner: Spawner, + containerId: string, + maxSeconds = 15, +): Effect.Effect { + const stillStarting = inspectContainerState(spawner, containerId).pipe( + Effect.map( + (state) => state.running && state.health === "starting" && !healthCheckCannotExec(state), + ), + Effect.orElseSucceed(() => false), + Effect.withTracerEnabled(false), + Effect.provideService(ChildTracePropagation, false), + ); + return stillStarting.pipe( + Effect.repeat({ schedule: Schedule.spaced("1 seconds"), while: (starting) => starting }), + Effect.timeout(Duration.seconds(maxSeconds)), + Effect.ignore, + Effect.withSpan("HealthCheck.awaitDockerHealthSettled"), + ); } diff --git a/apps/cli/src/command-internal/db-bootstrap/health-check.unit.test.ts b/apps/cli/src/command-internal/db-bootstrap/health-check.unit.test.ts index 7a8a369dde..81aba24804 100644 --- a/apps/cli/src/command-internal/db-bootstrap/health-check.unit.test.ts +++ b/apps/cli/src/command-internal/db-bootstrap/health-check.unit.test.ts @@ -3,16 +3,27 @@ import { Deferred, Effect, Exit, Fiber, Layer, Sink, Stream, Tracer } from "effe import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/process"; import * as HttpClient from "effect/http/HttpClient"; +import * as HttpClientError from "effect/http/HttpClientError"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; import * as TestClock from "effect/testing/TestClock"; +import { + EXEC_FAILED_STATE, + EXEC_FAILURE_OUTPUT, + PROBE_TIMEOUT_OUTPUT, + inspectedState, +} from "../../../tests/helpers/docker-health.ts"; import { DbConnectError } from "../db-connection.errors.ts"; import { DbConnection, type DbSession, type PgConnInput } from "../db-connection.service.ts"; import { HealthCheckTimeoutError, + awaitDockerHealthSettled, + healthCheckExecUnavailableWarning, waitForHealthyServices, - waitForShadowReady, + waitForPostgresReady, + type ExecFallbackProbe, type HealthCheckPostgrestGateway, + type WaitForHealthyServicesOptions, } from "./health-check.ts"; /** Ends a scripted `logs` script, failing the stream after already-emitted chunks — the real "daemon dropped the pipe mid-dump" case. */ @@ -37,6 +48,8 @@ function mockHealthSpawner( opts: { readonly runtime?: "docker" | "podman"; readonly beforeInspect?: (containerId: string) => Effect.Effect; + /** Makes every `docker container inspect` exit non-zero, as when the daemon is unreachable. */ + readonly inspectFails?: boolean; } = {}, ) { const counts = new Map(); @@ -82,6 +95,7 @@ function mockHealthSpawner( let stdoutChunks: LogChunks = []; let stderrChunks: LogChunks = []; + let exitCode = 0; if (args[0] === "container" && args[1] === "inspect") { const containerId = args[2] ?? ""; if (opts.beforeInspect !== undefined) { @@ -89,7 +103,12 @@ function mockHealthSpawner( } const callIndex = counts.get(containerId) ?? 0; counts.set(containerId, callIndex + 1); - stdoutChunks = [inspectResponse(containerId, callIndex)]; + if (opts.inspectFails === true) { + exitCode = 1; + stderrChunks = ["Cannot connect to the Docker daemon\n"]; + } else { + stdoutChunks = [inspectResponse(containerId, callIndex)]; + } } else if (args[0] === "logs") { const script = logs[args[1] ?? ""]; if (script !== undefined) { @@ -103,7 +122,7 @@ function mockHealthSpawner( } const exitDeferred = yield* Deferred.make(); - yield* Deferred.succeed(exitDeferred, ChildProcessSpawner.ExitCode(0)); + yield* Deferred.succeed(exitDeferred, ChildProcessSpawner.ExitCode(exitCode)); return ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(1), stdout: toStream(stdoutChunks), @@ -332,6 +351,73 @@ describe("waitForHealthyServices", () => { }), ); + it.effect( + "fails with a hung-probe reason once the wall-clock bound passes when the first round never completes", + () => + withSilencedStderr( + Effect.gen(function* () { + const mock = mockHealthSpawner( + () => runningHealthy, + {}, + { beforeInspect: () => Effect.never }, + ); + + const fiber = yield* waitForHealthyServices(mock.spawner, ["supabase_kong_proj"], { + timeoutSeconds: 1, + }).pipe( + Effect.provide(unusedHttpClientLayer), + Effect.forkChild({ startImmediately: true }), + ); + for (let second = 0; second < 21; second++) { + yield* TestClock.adjust("1 seconds"); + } + const pendingAt21Seconds = fiber.pollUnsafe(); + yield* TestClock.adjust("1 seconds"); + const error = yield* Fiber.join(fiber).pipe(Effect.flip); + + const reason = "container is not ready: readiness probe hung after 22s"; + expect(pendingAt21Seconds).toBeUndefined(); + expect(error).toBeInstanceOf(HealthCheckTimeoutError); + expect(error.unhealthy).toEqual([{ containerId: "supabase_kong_proj", reason }]); + expect(error.message).toBe(`supabase_kong_proj ${reason}`); + }), + ), + ); + + it.effect( + "reports the last completed round's reason when a later round hangs past the wall-clock bound", + () => + withSilencedStderr( + Effect.gen(function* () { + let inspects = 0; + const mock = mockHealthSpawner( + () => runningStarting, + {}, + { beforeInspect: () => (++inspects > 1 ? Effect.never : Effect.void) }, + ); + + const fiber = yield* waitForHealthyServices(mock.spawner, ["supabase_kong_proj"], { + timeoutSeconds: 1, + }).pipe( + Effect.provide(unusedHttpClientLayer), + Effect.forkChild({ startImmediately: true }), + ); + for (let second = 0; second < 21; second++) { + yield* TestClock.adjust("1 seconds"); + } + const pendingAt21Seconds = fiber.pollUnsafe(); + yield* TestClock.adjust("1 seconds"); + const error = yield* Fiber.join(fiber).pipe(Effect.flip); + + expect(pendingAt21Seconds).toBeUndefined(); + expect(error).toBeInstanceOf(HealthCheckTimeoutError); + expect(error.unhealthy).toEqual([ + { containerId: "supabase_kong_proj", reason: "container is not ready: starting" }, + ]); + }), + ), + ); + describe("exec format error recovery advice", () => { /** * The dumped logs are teed to the real `process.stderr`; swallow them so @@ -880,7 +966,7 @@ function withSilencedStderr(effect: Effect.Effect): Effect.Eff const inspectCalls = (mock: ReturnType) => mock.spawned.filter((args) => args[0] === "container" && args[1] === "inspect"); -describe("waitForShadowReady", () => { +describe("waitForPostgresReady", () => { it.effect( "resolves on the first successful connect, while Docker still reports the healthcheck as starting", () => @@ -891,7 +977,7 @@ describe("waitForShadowReady", () => { const mock = mockHealthSpawner(() => runningStarting); const db = mockShadowDbConnection(); - const exit = yield* waitForShadowReady( + const exit = yield* waitForPostgresReady( mock.spawner, SHADOW_CONTAINER_ID, shadowConnConfig, @@ -913,9 +999,14 @@ describe("waitForShadowReady", () => { const mock = mockHealthSpawner(() => runningStarting); const db = mockShadowDbConnection({ failTimes: 2 }); - const fiber = yield* waitForShadowReady(mock.spawner, SHADOW_CONTAINER_ID, shadowConnConfig, { - timeoutSeconds: 30, - }).pipe(Effect.provide(db.layer), Effect.forkChild({ startImmediately: true })); + const fiber = yield* waitForPostgresReady( + mock.spawner, + SHADOW_CONTAINER_ID, + shadowConnConfig, + { + timeoutSeconds: 30, + }, + ).pipe(Effect.provide(db.layer), Effect.forkChild({ startImmediately: true })); yield* TestClock.adjust("1 seconds"); yield* TestClock.adjust("1 seconds"); @@ -938,7 +1029,7 @@ describe("waitForShadowReady", () => { const error = yield* withSilencedStderr( Effect.gen(function* () { - const fiber = yield* waitForShadowReady( + const fiber = yield* waitForPostgresReady( mock.spawner, SHADOW_CONTAINER_ID, shadowConnConfig, @@ -953,9 +1044,14 @@ describe("waitForShadowReady", () => { expect(error).toBeInstanceOf(HealthCheckTimeoutError); expect(error.unhealthy).toEqual([ - { containerId: SHADOW_CONTAINER_ID, reason: "connection refused" }, + { + containerId: SHADOW_CONTAINER_ID, + reason: "container is not ready: connection refused", + }, ]); - expect(error.message).toBe(`${SHADOW_CONTAINER_ID} connection refused`); + expect(error.message).toBe( + `${SHADOW_CONTAINER_ID} container is not ready: connection refused`, + ); // 1 initial attempt plus `timeoutSeconds * 2` retries at 500ms intervals. expect(db.attempts).toHaveLength(5); expect( @@ -975,7 +1071,7 @@ describe("waitForShadowReady", () => { const error = yield* withSilencedStderr( Effect.gen(function* () { - const fiber = yield* waitForShadowReady( + const fiber = yield* waitForPostgresReady( mock.spawner, SHADOW_CONTAINER_ID, shadowConnConfig, @@ -995,9 +1091,11 @@ describe("waitForShadowReady", () => { // Same failure the exhausted path produces: the last completed attempt's own reason, // the same ` ` message, and the same log dump. expect(error).toBeInstanceOf(HealthCheckTimeoutError); - expect(error.message).toBe(`${SHADOW_CONTAINER_ID} connection refused`); + expect(error.message).toBe( + `${SHADOW_CONTAINER_ID} container is not ready: connection refused`, + ); expect(error.unhealthy).toEqual([ - { containerId: SHADOW_CONTAINER_ID, reason: "connection refused" }, + { containerId: SHADOW_CONTAINER_ID, reason: "container is not ready: connection refused" }, ]); expect( mock.spawned.some((args) => args[0] === "logs" && args[1] === SHADOW_CONTAINER_ID), @@ -1015,7 +1113,7 @@ describe("waitForShadowReady", () => { const error = yield* withSilencedStderr( Effect.gen(function* () { - const fiber = yield* waitForShadowReady( + const fiber = yield* waitForPostgresReady( mock.spawner, SHADOW_CONTAINER_ID, shadowConnConfig, @@ -1040,6 +1138,36 @@ describe("waitForShadowReady", () => { }), ); + it.effect("reports a first dial that never returns as not ready, naming the wall-clock cap", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => runningStarting); + const db = mockShadowDbConnection({ + failTimes: Number.POSITIVE_INFINITY, + connectMillis: 60_000, + }); + + const error = yield* withSilencedStderr( + Effect.gen(function* () { + const fiber = yield* waitForPostgresReady( + mock.spawner, + SHADOW_CONTAINER_ID, + shadowConnConfig, + { timeoutSeconds: 1 }, + ).pipe(Effect.provide(db.layer), Effect.forkChild({ startImmediately: true })); + + yield* TestClock.adjust("3 seconds"); + return yield* Fiber.join(fiber).pipe(Effect.flip); + }), + ); + + const reason = "container is not ready: connection attempt hung after 3s"; + expect(error).toBeInstanceOf(HealthCheckTimeoutError); + expect(error.message).toBe(`${SHADOW_CONTAINER_ID} ${reason}`); + expect(error.unhealthy).toEqual([{ containerId: SHADOW_CONTAINER_ID, reason }]); + expect(db.attempts).toHaveLength(1); + }), + ); + describe("exec format error recovery advice", () => { it.effect("names the shadow's image when its dumped logs show exec format error", () => Effect.gen(function* () { @@ -1050,7 +1178,7 @@ describe("waitForShadowReady", () => { const error = yield* withSilencedStderr( Effect.gen(function* () { - const fiber = yield* waitForShadowReady( + const fiber = yield* waitForPostgresReady( mock.spawner, SHADOW_CONTAINER_ID, shadowConnConfig, @@ -1081,7 +1209,7 @@ describe("waitForShadowReady", () => { const error = yield* withSilencedStderr( Effect.gen(function* () { - const fiber = yield* waitForShadowReady( + const fiber = yield* waitForPostgresReady( mock.spawner, SHADOW_CONTAINER_ID, shadowConnConfig, @@ -1105,7 +1233,7 @@ describe("waitForShadowReady", () => { const error = yield* withSilencedStderr( Effect.gen(function* () { - const fiber = yield* waitForShadowReady( + const fiber = yield* waitForPostgresReady( mock.spawner, SHADOW_CONTAINER_ID, shadowConnConfig, @@ -1123,3 +1251,496 @@ describe("waitForShadowReady", () => { ); }); }); + +describe("waitForHealthyServices where Docker cannot exec health checks", () => { + const STUDIO = "supabase_studio_proj"; + const STORAGE = "supabase_storage_proj"; + const GATEWAY_URL = "http://127.0.0.1:54321"; + const STORAGE_STATUS_PATH = "/storage/v1/status"; + + const orderlyUnhealthy = inspectedState({ + health: "unhealthy", + probes: [{ exitCode: 1, output: "pg_isready: no response" }], + }); + const probeTimedOut = inspectedState({ + health: "unhealthy", + probes: [{ exitCode: -1, output: PROBE_TIMEOUT_OUTPUT }], + }); + + const runningFallback = (...containerIds: ReadonlyArray) => + new Map(containerIds.map((id) => [id, { kind: "running" }])); + const gatewayFallback = (containerId: string, path: string) => + new Map([ + [ + containerId, + { + kind: "gateway", + gateway: { apiExternalUrl: GATEWAY_URL, secretKey: "sb_secret_local" }, + path, + }, + ], + ]); + + const KONG = "supabase_kong_proj"; + const reachableFallback = (containerId: string) => + new Map([ + [ + containerId, + { + kind: "gateway-reachable", + gateway: { apiExternalUrl: GATEWAY_URL, secretKey: "sb_secret_local" }, + }, + ], + ]); + + function recordingGateway(statusByPath: Readonly> = {}) { + const requests: Array<{ + readonly method: string; + readonly url: string; + readonly headers: Readonly>; + }> = []; + const layer = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => { + requests.push({ + method: request.method, + url: request.url, + headers: { ...request.headers }, + }); + const status = statusByPath[new URL(request.url).pathname] ?? 200; + return Effect.succeed(HttpClientResponse.fromWeb(request, new Response("{}", { status }))); + }), + ); + return { layer, requests }; + } + + const settled = ( + mock: ReturnType, + containerIds: ReadonlyArray, + opts: WaitForHealthyServicesOptions = {}, + http: Layer.Layer = unusedHttpClientLayer, + ) => + waitForHealthyServices(mock.spawner, containerIds, { timeoutSeconds: 1, ...opts }).pipe( + Effect.provide(http), + ); + + const timeoutFailure = ( + mock: ReturnType, + containerIds: ReadonlyArray, + opts: WaitForHealthyServicesOptions = {}, + http: Layer.Layer = unusedHttpClientLayer, + ) => + withSilencedStderr( + Effect.gen(function* () { + const fiber = yield* settled(mock, containerIds, opts, http).pipe( + Effect.forkChild({ startImmediately: true }), + ); + yield* TestClock.adjust("1 seconds"); + return yield* Fiber.join(fiber).pipe(Effect.flip); + }), + ); + + it.effect("names the probe's OCI error and the exec check when no fallback is configured", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => EXEC_FAILED_STATE); + + const error = yield* timeoutFailure(mock, [STUDIO]); + + const reason = `container is not ready: unhealthy (last health check exit code -1: ${EXEC_FAILURE_OUTPUT})`; + expect(error).toBeInstanceOf(HealthCheckTimeoutError); + expect(error.unhealthy).toEqual([{ containerId: STUDIO, reason }]); + expect(error.message).toBe(`${STUDIO} ${reason}`); + expect(error.suggestion).toBe( + [ + `Docker could not run the health check inside ${STUDIO} ("OCI runtime exec failed"), so it cannot report them healthy.`, + "This happens when Docker runs nested or sandboxed.", + `Check with: docker exec ${STUDIO} true`, + ].join("\n"), + ); + }), + ); + + it.effect("names an ordinary failing probe without the exec-unavailable suggestion", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => orderlyUnhealthy); + + const error = yield* timeoutFailure(mock, [STUDIO]); + + expect(error.message).toBe( + `${STUDIO} container is not ready: unhealthy (last health check exit code 1: pg_isready: no response)`, + ); + expect(error.suggestion).toBeUndefined(); + }), + ); + + it.effect("still fails a probe timeout (exit -1) when a running fallback is configured", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => probeTimedOut); + + const error = yield* timeoutFailure(mock, [STUDIO], { + execFallbacks: runningFallback(STUDIO), + }); + + expect(error.message).toBe( + `${STUDIO} container is not ready: unhealthy (last health check exit code -1: Health check exceeded timeout (2s))`, + ); + expect(error.suggestion).toBeUndefined(); + }), + ); + + it.effect("still fails a restarting container whose last probe was an exec failure", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => + inspectedState({ + status: "restarting", + health: "unhealthy", + probes: [{ exitCode: -1, output: EXEC_FAILURE_OUTPUT }], + }), + ); + + const error = yield* timeoutFailure(mock, [STUDIO], { + execFallbacks: runningFallback(STUDIO), + }); + + expect(error.message).toContain(`${STUDIO} container is not ready: unhealthy (last health`); + expect(error.suggestion).toBeUndefined(); + }), + ); + + it.effect( + "accepts an exec-failed container on a running fallback and reports it as assumed", + () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => EXEC_FAILED_STATE); + + const result = yield* settled(mock, [STUDIO], { execFallbacks: runningFallback(STUDIO) }); + + expect(result).toEqual({ execUnavailable: [STUDIO], readinessAssumed: [STUDIO] }); + }), + ); + + it.effect("verifies an exec-failed container through the gateway with an authenticated GET", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => EXEC_FAILED_STATE); + const gateway = recordingGateway(); + + const result = yield* settled( + mock, + [STORAGE], + { execFallbacks: gatewayFallback(STORAGE, STORAGE_STATUS_PATH) }, + gateway.layer, + ); + + expect(result).toEqual({ execUnavailable: [STORAGE], readinessAssumed: [] }); + expect(gateway.requests).toEqual([ + { + method: "GET", + url: `${GATEWAY_URL}${STORAGE_STATUS_PATH}`, + headers: expect.objectContaining({ apikey: "sb_secret_local" }), + }, + ]); + }), + ); + + for (const status of [503, 404]) { + it.effect(`fails with the gateway's ${status} and the exec suggestion when it is not 200`, () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => EXEC_FAILED_STATE); + const gateway = recordingGateway({ [STORAGE_STATUS_PATH]: status }); + + const error = yield* timeoutFailure( + mock, + [STORAGE], + { execFallbacks: gatewayFallback(STORAGE, STORAGE_STATUS_PATH) }, + gateway.layer, + ); + + expect(error.unhealthy).toEqual([ + { + containerId: STORAGE, + reason: `container is not ready: ${STORAGE_STATUS_PATH}: unexpected status ${status}`, + }, + ]); + expect(error.suggestion).toContain(`Check with: docker exec ${STORAGE} true`); + }), + ); + } + + it.effect("accepts the gateway's 404 as proof that an exec-failed Kong is up", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => EXEC_FAILED_STATE); + const gateway = recordingGateway({ "/": 404 }); + + const result = yield* settled( + mock, + [KONG], + { execFallbacks: reachableFallback(KONG) }, + gateway.layer, + ); + + expect(result).toEqual({ execUnavailable: [KONG], readinessAssumed: [] }); + expect(gateway.requests).toEqual([ + { + method: "GET", + url: `${GATEWAY_URL}/`, + headers: expect.objectContaining({ apikey: "sb_secret_local" }), + }, + ]); + }), + ); + + it.effect("fails an exec-failed Kong whose gateway does not answer", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => EXEC_FAILED_STATE); + const unreachable = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ + request, + cause: new Error("connect ECONNREFUSED"), + }), + }), + ), + ), + ); + + const error = yield* timeoutFailure( + mock, + [KONG], + { execFallbacks: reachableFallback(KONG) }, + unreachable, + ); + + expect(error.unhealthy).toEqual([ + { + containerId: KONG, + reason: `container is not ready: HttpClientError: Transport error (GET ${GATEWAY_URL}/)`, + }, + ]); + expect(error.suggestion).toContain(`Check with: docker exec ${KONG} true`); + }), + ); + + it.effect("fails a gateway GET whose body never completes once one probe timeout elapses", () => + withSilencedStderr( + Effect.gen(function* () { + const mock = mockHealthSpawner(() => EXEC_FAILED_STATE); + const stalledBody = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response( + new ReadableStream({ + start: (controller) => controller.enqueue(new TextEncoder().encode("{")), + }), + { status: 200 }, + ), + ), + ), + ), + ); + + const fiber = yield* settled( + mock, + [STORAGE], + { timeoutSeconds: 0, execFallbacks: gatewayFallback(STORAGE, STORAGE_STATUS_PATH) }, + stalledBody, + ).pipe(Effect.forkChild({ startImmediately: true })); + for (let second = 0; second < 9; second++) { + yield* TestClock.adjust("1 seconds"); + } + const pendingAtNineSeconds = fiber.pollUnsafe(); + yield* TestClock.adjust("1 seconds"); + const error = yield* Fiber.join(fiber).pipe(Effect.flip); + + expect(pendingAtNineSeconds).toBeUndefined(); + expect(error).toBeInstanceOf(HealthCheckTimeoutError); + expect(error.unhealthy).toEqual([ + { + containerId: STORAGE, + reason: expect.stringMatching( + /^container is not ready: \/storage\/v1\/status: TimeoutError/u, + ), + }, + ]); + }), + ), + ); + + it.effect("joins the exec-format and exec-unavailable advice for different containers", () => + Effect.gen(function* () { + const broken = "supabase_inbucket_proj"; + const mock = mockHealthSpawner( + (containerId) => (containerId === broken ? notRunning : EXEC_FAILED_STATE), + { [broken]: ["exec /mailpit: exec format error\n"] }, + ); + + const error = yield* timeoutFailure(mock, [broken, STUDIO], { + images: new Map([[broken, "public.ecr.aws/supabase/mailpit:v1.30.2"]]), + }); + + const suggestion = error.suggestion ?? ""; + expect(suggestion).toContain(`${broken}'s image public.ecr.aws/supabase/mailpit:v1.30.2`); + expect(suggestion).toContain( + `architecture.\n\nDocker could not run the health check inside ${STUDIO} (`, + ); + expect(suggestion).not.toContain(`inside ${broken}`); + }), + ); + + it.effect("does not blame exec for an ordinary failure beside an accepted exec-failed one", () => + Effect.gen(function* () { + const mock = mockHealthSpawner((containerId) => + containerId === STUDIO ? EXEC_FAILED_STATE : orderlyUnhealthy, + ); + + const error = yield* timeoutFailure(mock, [STUDIO, STORAGE], { + execFallbacks: runningFallback(STUDIO), + }); + + expect(error.unhealthy.map((failure) => failure.containerId)).toEqual([STORAGE]); + expect(error.suggestion).toBeUndefined(); + }), + ); + + it.effect("reports nothing as exec-unavailable while Docker's health check works", () => + Effect.gen(function* () { + const orderlyStarting = inspectedState({ + health: "starting", + probes: [{ exitCode: 1, output: "pg_isready: no response" }], + }); + const mock = mockHealthSpawner((_id, callIndex) => + callIndex === 0 ? orderlyStarting : runningHealthy, + ); + + const fiber = yield* settled(mock, [STUDIO], { + execFallbacks: runningFallback(STUDIO), + }).pipe(Effect.forkChild({ startImmediately: true })); + yield* TestClock.adjust("1 seconds"); + const result = yield* Fiber.join(fiber); + + expect(result).toEqual({ execUnavailable: [], readinessAssumed: [] }); + }), + ); +}); + +describe("healthCheckExecUnavailableWarning", () => { + it("names every container whose readiness was not verified", () => { + const warning = healthCheckExecUnavailableWarning([ + "supabase_realtime_proj", + "supabase_studio_proj", + ]); + + expect(warning).toContain("WARNING:"); + expect(warning).toContain("(OCI runtime exec failed)"); + expect(warning).toContain( + "Readiness of supabase_realtime_proj, supabase_studio_proj was not verified.", + ); + expect(warning.endsWith("\n")).toBe(true); + }); + + it("says readiness was checked from the host when every container was verified", () => { + const warning = healthCheckExecUnavailableWarning([]); + + expect(warning).toContain("Readiness was checked from the host instead."); + expect(warning).not.toContain("was not verified"); + }); +}); + +describe("awaitDockerHealthSettled", () => { + const DB = "supabase_db_proj"; + + const run = (mock: ReturnType) => + Effect.gen(function* () { + const fiber = yield* awaitDockerHealthSettled(mock.spawner, DB).pipe( + Effect.forkChild({ startImmediately: true }), + ); + yield* TestClock.adjust("1 seconds"); + return yield* Fiber.await(fiber); + }); + + const settlesImmediately: ReadonlyArray = [ + ["Docker already reports it healthy", runningHealthy], + ["its health check cannot run on this host", EXEC_FAILED_STATE], + [ + "an exec failure is recorded while Docker still reports starting", + inspectedState({ + health: "starting", + probes: [{ exitCode: -1, output: EXEC_FAILURE_OUTPUT }], + }), + ], + ["it is no longer running", notRunning], + [ + "Docker reports it unhealthy", + inspectedState({ health: "unhealthy", probes: [{ exitCode: 1, output: "no response" }] }), + ], + ["it has no health check", inspectedState()], + ]; + for (const [scenario, state] of settlesImmediately) { + it.effect(`returns after a single inspect when ${scenario}`, () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => state); + + const exit = yield* run(mock); + + expect(Exit.isSuccess(exit)).toBe(true); + expect(inspectCalls(mock)).toHaveLength(1); + }), + ); + } + + it.effect("succeeds without retrying when the inspect itself fails", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => runningStarting, {}, { inspectFails: true }); + + const exit = yield* run(mock); + + expect(Exit.isSuccess(exit)).toBe(true); + expect(inspectCalls(mock)).toHaveLength(1); + }), + ); + + it.effect("keeps polling while Docker reports starting and returns once it settles", () => + Effect.gen(function* () { + const mock = mockHealthSpawner((_id, callIndex) => + callIndex < 2 ? runningStarting : runningHealthy, + ); + + const fiber = yield* awaitDockerHealthSettled(mock.spawner, DB).pipe( + Effect.forkChild({ startImmediately: true }), + ); + yield* TestClock.adjust("1 seconds"); + yield* TestClock.adjust("1 seconds"); + const exit = yield* Fiber.await(fiber); + + expect(Exit.isSuccess(exit)).toBe(true); + expect(inspectCalls(mock)).toHaveLength(3); + }), + ); + + it.effect("gives up without failing after 15 seconds of starting, and stops polling", () => + Effect.gen(function* () { + const mock = mockHealthSpawner(() => runningStarting); + + const fiber = yield* awaitDockerHealthSettled(mock.spawner, DB).pipe( + Effect.forkChild({ startImmediately: true }), + ); + for (let second = 0; second < 14; second++) { + yield* TestClock.adjust("1 seconds"); + } + const pendingAt14Seconds = fiber.pollUnsafe(); + yield* TestClock.adjust("1 seconds"); + const exit = yield* Fiber.await(fiber); + const pollsAtCap = inspectCalls(mock).length; + yield* TestClock.adjust("10 seconds"); + + expect(pendingAt14Seconds).toBeUndefined(); + expect(Exit.isSuccess(exit)).toBe(true); + expect(inspectCalls(mock)).toHaveLength(pollsAtCap); + }), + ); +}); 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 ef41c9dd1c..6986dff839 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 @@ -111,10 +111,9 @@ export const buildLocalDbContainerInputs = ( const document = resolvedConfig.loaded.document ?? {}; const experimental = yield* resolveExperimentalWithProjectEnv(projectEnvValues); - const values = yield* Effect.try({ - try: () => resolveLocalConfigValues(config, hostname, workdir, document), - catch: (cause) => mapError(cause instanceof Error ? cause.message : String(cause)), - }); + const values = yield* resolveLocalConfigValues(config, hostname, workdir, document).pipe( + Effect.mapError((cause) => mapError(cause.message)), + ); const bootstrapConfig = yield* resolveDbBootstrapConfig( fs, @@ -203,6 +202,8 @@ export const buildLocalDbContainerInputs = ( // Lazy: only evaluated when `runFreshDbSetup` reaches realtime setup and it's enabled. jwks: resolveLocalJwks(config, workdir, values.jwtSecret).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, diff --git a/apps/cli/src/command-internal/db-bootstrap/recreate-local-database.ts b/apps/cli/src/command-internal/db-bootstrap/recreate-local-database.ts index 5026606a3e..4c05dd8efc 100644 --- a/apps/cli/src/command-internal/db-bootstrap/recreate-local-database.ts +++ b/apps/cli/src/command-internal/db-bootstrap/recreate-local-database.ts @@ -2,7 +2,7 @@ * `db reset --local`'s container-recreate composition — not a thin wrapper over * {@link startDatabase}, since the shared `StartDatabase`-equivalent primitive is never called * from a reset either. Reuses the same underlying primitives (`ensureNetwork`, - * `buildPostgresStartContainerSpec`, `createContainer`, `waitForHealthyServices`, + * `buildPostgresStartContainerSpec`, `createContainer`, `waitForPostgresReady`, * `startSetupLocalDatabase`) with a distinct sequence per major version: PG >= 15 recreates the * container/volume/network and re-runs setup; PG <= 14 recreates the two databases in place and * reinitializes the schema. Neither path probes for an existing volume, supports @@ -52,7 +52,11 @@ import { type FreshDbSetupInput, type StartSetupLocalDatabaseError, } from "./db-setup.ts"; -import { waitForHealthyServices, type HealthCheckTimeoutError } from "./health-check.ts"; +import { + awaitDockerHealthSettled, + waitForPostgresReady, + type HealthCheckTimeoutError, +} from "./health-check.ts"; import type { ImagePrepullError } from "./image-prepull.ts"; import { startInternalDbPassword } from "./internal-db-connection.ts"; import { @@ -283,10 +287,18 @@ const recreateLocalDatabase15 = ( yield* createContainer(spawner, postgresSpec, input.containerOpts); // Never swallowed — reset has no `--from-backup`-equivalent gate. - yield* waitForHealthyServices(spawner, [postgresSpec.containerName], { - timeoutSeconds: input.dbHealthTimeoutSeconds, - images: new Map([[postgresSpec.containerName, resolvedPostgresImage]]), - }); + yield* waitForPostgresReady( + spawner, + postgresSpec.containerName, + { + host: input.hostname, + port: input.dbPort, + user: "postgres", + password: startInternalDbPassword(input.setup.dbUrl), + database: "postgres", + }, + { timeoutSeconds: input.dbHealthTimeoutSeconds, image: resolvedPostgresImage }, + ); // No fresh-volume gate needed: a reset just removed the volume above, so it's always fresh. // Passes the resolved reset `version`, unlike `db start`'s own call. @@ -377,9 +389,18 @@ const recreateLocalDatabase14 = ( // the satellite restarts inside `restartServicesAndReloadKong`. yield* output.raw("Restarting containers...\n", "stderr"); yield* restartContainer(spawner, input.dbContainerId); - yield* waitForHealthyServices(spawner, [input.dbContainerId], { - timeoutSeconds: input.dbHealthTimeoutSeconds, - }); + yield* waitForPostgresReady( + spawner, + input.dbContainerId, + { + host: input.hostname, + port: input.dbPort, + user: "postgres", + password: dbPassword, + database: "postgres", + }, + { timeoutSeconds: input.dbHealthTimeoutSeconds }, + ); yield* restartServicesAndReloadKong(spawner, input.projectId); // Final connect as `postgres`/`postgres` to migrate and seed. @@ -421,6 +442,7 @@ export const recreateLocalDatabase = ( ? recreateLocalDatabase14(spawner, input) : recreateLocalDatabase15(spawner, input) ).pipe( + Effect.tap(() => awaitDockerHealthSettled(spawner, input.dbContainerId)), Effect.withSpan("DbBootstrap.recreateLocalDatabase", { attributes: { "db.major_version": input.setup.majorVersion }, }), 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 d086f337c8..3b4efb4bea 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/db-bootstrap/shadow-cache.ts b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts index 51e5a3677f..42f45b851f 100644 --- a/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts +++ b/apps/cli/src/command-internal/db-bootstrap/shadow-cache.ts @@ -40,7 +40,7 @@ import { START_DB_SCHEMA_SQL } from "./templates/db-schema.sql.ts"; import { START_DB_SUPABASE_SQL } from "./templates/db-supabase.sql.ts"; import { START_DB_WEBHOOK_SQL } from "./templates/db-webhook.sql.ts"; import { CREATE_VAULT_KV, READ_VAULT_KV, UPDATE_VAULT_KV, type VaultSecret } from "../vault.ts"; -import { waitForShadowReady } from "./health-check.ts"; +import { waitForPostgresReady } from "./health-check.ts"; import { exportPgDataTar, pgDataRestoreArchive, @@ -532,7 +532,7 @@ const awaitShadowReady = ( containerId: string, what: string, ): Effect.Effect => - waitForShadowReady(spawner, containerId, shadowConnConfig(input), { + waitForPostgresReady(spawner, containerId, shadowConnConfig(input), { timeoutSeconds: input.healthTimeoutSeconds, image: input.image, }).pipe( 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/db-bootstrap/start-database.ts b/apps/cli/src/command-internal/db-bootstrap/start-database.ts index df3d0ce694..78eb18b97c 100644 --- a/apps/cli/src/command-internal/db-bootstrap/start-database.ts +++ b/apps/cli/src/command-internal/db-bootstrap/start-database.ts @@ -44,7 +44,8 @@ import { type StartSetupLocalDatabaseError, } from "./db-setup.ts"; import type { ImagePrepullError } from "./image-prepull.ts"; -import { waitForHealthyServices, type HealthCheckTimeoutError } from "./health-check.ts"; +import { waitForPostgresReady, type HealthCheckTimeoutError } from "./health-check.ts"; +import { startInternalDbPassword } from "./internal-db-connection.ts"; import { buildPostgresStartContainerSpec, type PostgresStartServiceInput, @@ -182,17 +183,23 @@ export const startDatabase = ( }); yield* createContainer(spawner, postgresSpec, input.containerOpts); - const postgresHealthResult = yield* waitForHealthyServices( + // A host connect, not Docker's HEALTHCHECK: that needs `docker exec`, which nested or + // sandboxed Docker cannot do. + const postgresHealthResult = yield* waitForPostgresReady( spawner, - [postgresSpec.containerName], + postgresSpec.containerName, { - timeoutSeconds: input.dbHealthTimeoutSeconds, - images: new Map([[postgresSpec.containerName, resolvedPostgresImage]]), + host: input.hostname, + port: input.dbPort, + user: "postgres", + password: startInternalDbPassword(input.setup.dbUrl), + database: "postgres", }, + { timeoutSeconds: input.dbHealthTimeoutSeconds, image: resolvedPostgresImage }, ).pipe(Effect.result); if (Result.isFailure(postgresHealthResult)) { // Discarded only when `fromBackup` is set — the log dump to stderr already happened - // inside `waitForHealthyServices` regardless. Any other failure propagates bare; see this + // inside `waitForPostgresReady` regardless. Any other failure propagates bare; see this // module's header for why `--ignore-health-check` is entirely the caller's concern. if (fromBackup === undefined) { return yield* postgresHealthResult.failure; 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 b650e7f228..6200563539 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 @@ -28,6 +28,7 @@ import { parseUrl } from "../storage-url.ts"; import { loadLocalProjectContext } from "../local-project-context.ts"; import { cliProjectFilterValue } from "../docker-ids.ts"; import { buildLocalDbContainerInputs } from "./local-container-inputs.ts"; +import { awaitDockerHealthSettled } from "./health-check.ts"; import { isLocalDbRunning } from "./local-db-running.ts"; import { rollbackStart } from "./rollback.ts"; import { startDatabase } from "./start-database.ts"; @@ -38,23 +39,17 @@ 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 synchronous parser's malformed-value failure into a typed `DbConfigLoadError` — 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 wrapDbConfigParse = (dottedFieldPath: string, thunk: () => T) => + Effect.try({ try: thunk, catch: dbConfigOverrideError(dottedFieldPath) }); type StartLocalDatabaseStatus = "already-running" | "started"; @@ -109,10 +104,10 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu // Every duration config field is decoded in this same unconditional pass, before Docker is // touched or the already-running check runs. The parsed values are discarded; only the // fail-fast behavior matters. - yield* wrapDbConfigOverride("auth.email.max_frequency", () => + yield* wrapDbConfigParse("auth.email.max_frequency", () => parseDuration(config.auth.email.max_frequency), ); - yield* wrapDbConfigOverride("auth.sms.max_frequency", () => + yield* wrapDbConfigParse("auth.sms.max_frequency", () => parseDuration(config.auth.sms.max_frequency), ); if ( @@ -128,24 +123,24 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu } const { timebox, inactivity_timeout: inactivityTimeout } = config.auth.sessions ?? {}; if (timebox !== undefined) { - yield* wrapDbConfigOverride("auth.sessions.timebox", () => parseDuration(timebox)); + yield* wrapDbConfigParse("auth.sessions.timebox", () => parseDuration(timebox)); } if (inactivityTimeout !== undefined) { - yield* wrapDbConfigOverride("auth.sessions.inactivity_timeout", () => + yield* wrapDbConfigParse("auth.sessions.inactivity_timeout", () => parseDuration(inactivityTimeout), ); } - yield* wrapDbConfigOverride("auth.mfa.phone.max_frequency", () => + yield* wrapDbConfigParse("auth.mfa.phone.max_frequency", () => parseDuration(config.auth.mfa.phone.max_frequency), ); - yield* wrapDbConfigOverride("auth.passkey", () => resolveGotruePasskeyWebauthn(document)); - yield* wrapDbConfigOverride("auth.external", () => + yield* wrapDbConfigParse("auth.passkey", () => resolveGotruePasskeyWebauthn(document)); + yield* wrapDbConfigParse("auth.external", () => resolveAuthExternalProviders(asRecord(document["auth"]), config.auth.external), ); - yield* wrapDbConfigOverride("storage.file_size_limit", () => + yield* wrapDbConfigParse("storage.file_size_limit", () => ramInBytes(config.storage.file_size_limit), ); - yield* wrapDbConfigOverride("db.health_timeout", () => + yield* wrapDbConfigParse("db.health_timeout", () => resolveHealthTimeoutSeconds(config.db.health_timeout), ); @@ -171,18 +166,16 @@ 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, document), - catch: (cause) => - new DbConfigLoadError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); + yield* resolveLocalConfigValues( + config, + hostnameForValidation, + cliSettings.workdir, + document, + ).pipe(Effect.mapError((cause) => new DbConfigLoadError({ message: cause.message }))); // If the db container is already up, tell the caller and stop here. Runs after the config // load/validation above. @@ -264,5 +257,9 @@ export const startLocalDatabase = Effect.fn("DbBootstrap.startLocalDatabase")(fu ), ); + // The connect probe returns before Docker's first health probe, and `status` and the + // already-running gate still read Docker health. + yield* awaitDockerHealthSettled(spawner, dbContainerId); + return { status: "started" } satisfies StartLocalDatabaseResult; }); diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-project-id-env.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-project-id-env.json index c6dc82d42a..b0130c633d 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-project-id-env.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-project-id-env.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.006", - "postgresImage": "supabase/postgres:17.11.0.006", + "postgresConfigImage": "supabase/postgres:17.11.0.007", + "postgresImage": "supabase/postgres:17.11.0.007", "realtimeEnabledForSetup": true, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_shell-project-id", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.006", + "configImage": "supabase/postgres:17.11.0.007", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-shell-env.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-shell-env.json index 3342de19a8..86dda247be 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-shell-env.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote-with-shell-env.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.006", - "postgresImage": "supabase/postgres:17.11.0.006", + "postgresConfigImage": "supabase/postgres:17.11.0.007", + "postgresImage": "supabase/postgres:17.11.0.007", "realtimeEnabledForSetup": false, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_abcdefghijklmnopqrst", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.006", + "configImage": "supabase/postgres:17.11.0.007", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote.json index 4431a2fa44..e2ed4087c9 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/matched-remote.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.006", - "postgresImage": "supabase/postgres:17.11.0.006", + "postgresConfigImage": "supabase/postgres:17.11.0.007", + "postgresImage": "supabase/postgres:17.11.0.007", "realtimeEnabledForSetup": true, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_abcdefghijklmnopqrst", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.006", + "configImage": "supabase/postgres:17.11.0.007", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/no-overrides.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/no-overrides.json index ec13fc8df4..477dbd3742 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/no-overrides.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/no-overrides.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.006", - "postgresImage": "supabase/postgres:17.11.0.006", + "postgresConfigImage": "supabase/postgres:17.11.0.007", + "postgresImage": "supabase/postgres:17.11.0.007", "realtimeEnabledForSetup": true, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_golden-shadow", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.006", + "configImage": "supabase/postgres:17.11.0.007", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/project-dotenv.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/project-dotenv.json index d11d0fc2ae..1e497b7bc3 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/project-dotenv.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/project-dotenv.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.006", - "postgresImage": "supabase/postgres:17.11.0.006", + "postgresConfigImage": "supabase/postgres:17.11.0.007", + "postgresImage": "supabase/postgres:17.11.0.007", "realtimeEnabledForSetup": false, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_golden-shadow", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.006", + "configImage": "supabase/postgres:17.11.0.007", "db": { "health_timeout": "2m", "major_version": 17, diff --git a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/shell-env.json b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/shell-env.json index 6ada9068d3..d2bd58688f 100644 --- a/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/shell-env.json +++ b/apps/cli/src/command-internal/db-bootstrap/testdata/config-precedence/local-container-inputs/shell-env.json @@ -5,8 +5,8 @@ "dbHealthTimeoutSeconds": 120, "majorVersion": 17, "orioledbVersion": null, - "postgresConfigImage": "supabase/postgres:17.11.0.006", - "postgresImage": "supabase/postgres:17.11.0.006", + "postgresConfigImage": "supabase/postgres:17.11.0.007", + "postgresImage": "supabase/postgres:17.11.0.007", "realtimeEnabledForSetup": false, "realtimeIpVersion": "IPv4", "realtimeMaxHeaderLength": 4096, @@ -32,7 +32,7 @@ "experimental": false, "networkId": "supabase_network_golden-shadow", "postgresSpecBase": { - "configImage": "supabase/postgres:17.11.0.006", + "configImage": "supabase/postgres:17.11.0.007", "db": { "health_timeout": "2m", "major_version": 17, 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 52453d1398..01e1942aea 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.ts @@ -10,7 +10,9 @@ import { type AuthInput, type CaptchaInput, type ConfigValidationInput, + ConfigValidateError, emailContentPathReadErrorMessage, + fsErrorCause, type ExperimentalInput, type HookInput, type MfaFactorInput, @@ -463,7 +465,7 @@ const readDbTomlCore = Effect.fnUntraced(function* ( const signingKeysPath = auth.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) }), @@ -526,17 +528,24 @@ const readDbTomlCore = Effect.fnUntraced(function* ( family === undefined ? undefined : cliConfigFamilyKey(family, name, "content"); const contentPresent = contentKey !== undefined && (yield* getKey(contentKey)).origin.tier !== "default"; - return yield* Effect.try({ - try: () => - resolveEmailTemplateContentPath({ - section, - name, - contentPath: entry.content_path ?? "", - contentPresent, - base: workdir, - }), - catch: (cause) => new DbConfigLoadError({ message: causeMessage(cause) }), - }); + return yield* resolveEmailTemplateContentPath({ + section, + name, + contentPath: entry.content_path ?? "", + contentPresent, + base: workdir, + }).pipe( + Effect.provideService(FileSystem.FileSystem, fs), + Effect.provideService(Path.Path, path), + Effect.mapError( + (error) => + new DbConfigLoadError({ + message: causeMessage( + error instanceof ConfigValidateError ? error : fsErrorCause(error), + ), + }), + ), + ); }); for (const [name, template] of Object.entries(auth.email.template)) { const contentPath = yield* emailContentPath("template", name, template); 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-lifecycle.ts b/apps/cli/src/command-internal/docker-lifecycle.ts index 07878356a6..aa5bc5c7c2 100644 --- a/apps/cli/src/command-internal/docker-lifecycle.ts +++ b/apps/cli/src/command-internal/docker-lifecycle.ts @@ -235,13 +235,77 @@ export const inspectContainerState = (spawner: Spawner, containerId: string) => }), ).pipe(Effect.withTracerEnabled(false), Effect.provideService(ChildTracePropagation, false)); -function parseContainerState(stdout: string): { +/** The most recent health-check probe Docker recorded for a container. */ +interface ContainerHealthProbe { + readonly exitCode: number; + readonly output: string; +} + +export interface ContainerState { readonly running: boolean; readonly status: string; readonly exitCode: number; readonly oomKilled: boolean; readonly health?: string; -} { + readonly lastHealthProbe?: ContainerHealthProbe; +} + +const HEALTH_PROBE_OUTPUT_MAX = 300; + +function parseLastHealthProbe(health: unknown): ContainerHealthProbe | undefined { + if (!isJsonRecord(health)) return undefined; + const log = health["Log"]; + if (!Array.isArray(log)) return undefined; + const last: unknown = log[log.length - 1]; + if (!isJsonRecord(last)) return undefined; + const exitCode = last["ExitCode"]; + const output = last["Output"]; + if (typeof exitCode !== "number" || typeof output !== "string") return undefined; + return { exitCode, output: output.replace(/\s+/gu, " ").trim() }; +} + +// runc reports a bad healthcheck command as a Go `exec.Error`, an `os.PathError`, or (before +// runc 1.1) wrapped in "exec user process caused". +const EXEC_IMAGE_FAULT_PATTERNS: ReadonlyArray = [ + /exec: \\?"[^"]*": (?:executable file not found in \$PATH|stat \S+: no such file or directory|permission denied)/u, + /(?:^|: )exec [^\s:]+: (?:no such file or directory|exec format error|permission denied)/u, + /exec user process caused:? \\?"?(?:no such file or directory|exec format error|permission denied)/u, +]; + +/** + * True when Docker could not launch the health check inside a running container. moby records an + * exec-start failure as probe exit code -1 with the runtime error as output; a probe timeout also + * uses -1 but its output never names the OCI runtime. Image faults in the healthcheck command are + * excluded: they are the image's problem, not the host's. + */ +export function healthCheckCannotExec(state: ContainerState): boolean { + const probe = state.lastHealthProbe; + if (!state.running || state.status !== "running" || state.health === "healthy") return false; + if (probe === undefined || probe.exitCode !== -1) return false; + const out = probe.output; + return ( + out.includes("OCI runtime exec failed") && + (out.includes("unable to start container process") || + out.includes("starting container process caused")) && + !EXEC_IMAGE_FAULT_PATTERNS.some((pattern) => pattern.test(out)) + ); +} + +/** The not-ready reason for an unhealthy container, naming its last health-check probe. */ +export function formatNotReadyReason(state: ContainerState): string { + const base = `container is not ready: ${state.health}`; + const probe = state.lastHealthProbe; + if (probe === undefined) return base; + const output = + probe.output.length > HEALTH_PROBE_OUTPUT_MAX + ? `${probe.output.slice(0, HEALTH_PROBE_OUTPUT_MAX - 1)}…` + : probe.output; + return output.length > 0 + ? `${base} (last health check exit code ${probe.exitCode}: ${output})` + : `${base} (last health check exit code ${probe.exitCode})`; +} + +function parseContainerState(stdout: string): ContainerState { const trimmed = stdout.trim(); let parsed: unknown; try { @@ -262,9 +326,15 @@ function parseContainerState(stdout: string): { const health = state["Health"]; const healthStatus = isJsonRecord(health) && typeof health["Status"] === "string" ? health["Status"] : undefined; - return healthStatus !== undefined - ? { running, status, exitCode, oomKilled, health: healthStatus } - : { running, status, exitCode, oomKilled }; + const lastHealthProbe = parseLastHealthProbe(health); + return { + running, + status, + exitCode, + oomKilled, + ...(healthStatus === undefined ? {} : { health: healthStatus }), + ...(lastHealthProbe === undefined ? {} : { lastHealthProbe }), + }; } function isJsonRecord(value: unknown): value is { readonly [key: string]: unknown } { diff --git a/apps/cli/src/command-internal/docker-lifecycle.unit.test.ts b/apps/cli/src/command-internal/docker-lifecycle.unit.test.ts index 21a4be6465..f6491f3792 100644 --- a/apps/cli/src/command-internal/docker-lifecycle.unit.test.ts +++ b/apps/cli/src/command-internal/docker-lifecycle.unit.test.ts @@ -2,14 +2,22 @@ import { describe, expect, it } from "@effect/vitest"; import { Deferred, Effect, Sink, Stream } from "effect"; import { ChildProcessSpawner } from "effect/process"; +import { + EXEC_FAILURE_OUTPUT, + PROBE_TIMEOUT_OUTPUT, + inspectedState, +} from "../../tests/helpers/docker-health.ts"; import { classifyCliErrorActionability } from "../shared/telemetry/error-actionability.ts"; import { DockerLifecycleInspectError, DockerLifecycleListError, + formatNotReadyReason, + healthCheckCannotExec, inspectContainerState, listContainerIdsAndNames, listContainersByLabel, listVolumesByLabel, + type ContainerState, } from "./docker-lifecycle.ts"; function mockSpawner( @@ -368,6 +376,307 @@ describe("inspectContainerState", () => { }); }); +describe("inspectContainerState health probe", () => { + const inspectLastProbe = (stdout: string) => + inspectContainerState(mockSpawner({ stdout }).spawner, "supabase_db_my-app"); + + it.live("reads the exec-start failure Docker recorded for a probe it could not launch", () => + inspectLastProbe( + inspectedState({ + health: "unhealthy", + probes: [{ exitCode: -1, output: EXEC_FAILURE_OUTPUT }], + }), + ).pipe( + Effect.map((state) => { + expect(state.health).toBe("unhealthy"); + expect(state.lastHealthProbe).toEqual({ exitCode: -1, output: EXEC_FAILURE_OUTPUT }); + expect(healthCheckCannotExec(state)).toBe(true); + }), + ), + ); + + it.live("keeps only the most recent entry of Health.Log", () => + inspectLastProbe( + inspectedState({ + health: "unhealthy", + probes: [ + { exitCode: 1, output: "first" }, + { exitCode: 2, output: "second" }, + { exitCode: 3, output: "third" }, + ], + }), + ).pipe( + Effect.map((state) => { + expect(state.lastHealthProbe).toEqual({ exitCode: 3, output: "third" }); + }), + ), + ); + + it.live("collapses whitespace runs and newlines in the probe output", () => + inspectLastProbe( + inspectedState({ + health: "unhealthy", + probes: [{ exitCode: 1, output: " pg_isready:\n\tno response\r\n " }], + }), + ).pipe( + Effect.map((state) => { + expect(state.lastHealthProbe?.output).toBe("pg_isready: no response"); + }), + ), + ); + + it.live("keeps probe output of exactly 300 characters whole", () => { + const output = "x".repeat(300); + return inspectLastProbe( + inspectedState({ health: "unhealthy", probes: [{ exitCode: 1, output }] }), + ).pipe( + Effect.map((state) => { + expect(state.lastHealthProbe?.output).toBe(output); + expect(formatNotReadyReason(state)).toContain(`: ${output})`); + }), + ); + }); + + it.live("keeps the full probe output and cuts only the rendered reason at 300 characters", () => + inspectLastProbe( + inspectedState({ + health: "unhealthy", + probes: [{ exitCode: 1, output: "x".repeat(301) }], + }), + ).pipe( + Effect.map((state) => { + expect(state.lastHealthProbe?.output).toBe("x".repeat(301)); + expect(formatNotReadyReason(state)).toContain(`: ${"x".repeat(299)}…)`); + }), + ), + ); + + const validProbe = { Start: "s", End: "e", ExitCode: 1, Output: "ok" }; + const malformedLogs: ReadonlyArray]> = [ + ["has no Log", { Status: "unhealthy" }], + ["has a Log that is not an array", { Status: "unhealthy", Log: "oops" }], + ["has an empty Log", { Status: "unhealthy", Log: [] }], + ["ends in an entry that is not an object", { Status: "unhealthy", Log: [validProbe, null] }], + ["ends in an entry with a string ExitCode", { Status: "unhealthy", Log: [{ ExitCode: "-1" }] }], + [ + "ends in an entry with no Output", + { Status: "unhealthy", Log: [validProbe, { ExitCode: -1 }] }, + ], + [ + "ends in an entry with a non-string Output", + { Status: "unhealthy", Log: [{ ExitCode: -1, Output: 7 }] }, + ], + ]; + for (const [scenario, health] of malformedLogs) { + it.live(`reports no probe when Health ${scenario}`, () => + inspectLastProbe(JSON.stringify({ Status: "running", Running: true, Health: health })).pipe( + Effect.map((state) => { + expect(state).toEqual({ + running: true, + status: "running", + exitCode: 0, + oomKilled: false, + health: "unhealthy", + }); + expect(state).not.toHaveProperty("lastHealthProbe"); + }), + ), + ); + } +}); + +describe("healthCheckCannotExec", () => { + const OLDER_RUNC_EXEC_FAILURE = + "OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: process_linux.go:103: executing setns process caused: exit status 1: unknown"; + + const state = (overrides: Partial = {}): ContainerState => ({ + running: true, + status: "running", + exitCode: 0, + oomKilled: false, + health: "unhealthy", + lastHealthProbe: { exitCode: -1, output: EXEC_FAILURE_OUTPUT }, + ...overrides, + }); + const probe = (exitCode: number, output: string) => ({ + lastHealthProbe: { exitCode, output }, + }); + const imageFaultPastTheDisplayedReason = `OCI runtime exec failed: exec failed: unable to start container process: ${"x".repeat(300)} exec: "/healthcheck.sh": permission denied: unknown`; + + const accepted: ReadonlyArray = [ + ["the exec-start failure Docker records on a sandboxed host", state()], + [ + "the same failure while Docker still reports the health as starting", + state({ health: "starting" }), + ], + ["the older runc wording of the same failure", state(probe(-1, OLDER_RUNC_EXEC_FAILURE))], + [ + "a cgroup path the host never created", + state( + probe( + -1, + "OCI runtime exec failed: exec failed: unable to start container process: error adding pid 123 to cgroups: failed to write 123: openat2 /sys/fs/cgroup/docker/abc/cgroup.procs: no such file or directory: unknown", + ), + ), + ], + [ + "a namespace file the host never exposed", + state( + probe( + -1, + "OCI runtime exec failed: exec failed: unable to start container process: error executing setns process: open /proc/123/ns/mnt: no such file or directory: unknown", + ), + ), + ], + [ + "runc unable to re-exec itself to enter the container", + state( + probe( + -1, + "OCI runtime exec failed: exec failed: unable to start container process: error executing setns process: fork/exec /proc/self/exe: permission denied: unknown", + ), + ), + ], + ]; + for (const [scenario, input] of accepted) { + it(`detects ${scenario}`, () => { + expect(healthCheckCannotExec(input)).toBe(true); + }); + } + + const rejected: ReadonlyArray = [ + ["a probe that outlived its timeout", state(probe(-1, PROBE_TIMEOUT_OUTPUT))], + [ + "a missing healthcheck binary", + state( + probe( + -1, + 'OCI runtime exec failed: exec failed: unable to start container process: exec: "pg_isready": executable file not found in $PATH: unknown', + ), + ), + ], + [ + "a healthcheck binary path that does not exist", + state( + probe( + -1, + 'OCI runtime exec failed: exec failed: unable to start container process: exec: "/usr/bin/pg_isready": stat /usr/bin/pg_isready: no such file or directory: unknown', + ), + ), + ], + [ + "a healthcheck binary built for another architecture", + state( + probe( + -1, + "OCI runtime exec failed: exec failed: unable to start container process: exec /usr/bin/pg_isready: exec format error: unknown", + ), + ), + ], + [ + "a healthcheck script that is not executable", + state( + probe( + -1, + 'OCI runtime exec failed: exec failed: unable to start container process: exec: "/healthcheck.sh": permission denied: unknown', + ), + ), + ], + [ + "a healthcheck script the kernel cannot open", + state( + probe( + -1, + "OCI runtime exec failed: exec failed: unable to start container process: exec /healthcheck.sh: permission denied: unknown", + ), + ), + ], + [ + "a healthcheck script whose shebang interpreter is missing", + state( + probe( + -1, + "OCI runtime exec failed: exec failed: unable to start container process: exec /healthcheck.sh: no such file or directory: unknown", + ), + ), + ], + [ + "an older runc reporting a healthcheck binary built for another architecture", + state( + probe( + -1, + "OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec user process caused: exec format error: unknown", + ), + ), + ], + [ + "an older runc reporting a missing shebang interpreter", + state( + probe( + -1, + 'OCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec user process caused \\"no such file or directory\\"": unknown', + ), + ), + ], + [ + "an image fault that only appears after the first 300 characters", + state(probe(-1, imageFaultPastTheDisplayedReason)), + ], + [ + "an exec into a stopped container", + state( + probe( + -1, + "OCI runtime exec failed: exec failed: cannot exec in a stopped container: unknown", + ), + ), + ], + ["a stopped container that once failed to exec", state({ running: false, status: "exited" })], + ["a restarting container", state({ status: "restarting" })], + ["a container Docker already reports healthy", state({ health: "healthy" })], + ["a probe that failed with an ordinary exit code", state(probe(1, EXEC_FAILURE_OUTPUT))], + ["a container with no recorded probe", state({ lastHealthProbe: undefined })], + ]; + for (const [scenario, input] of rejected) { + it(`does not mistake ${scenario} for an exec failure`, () => { + expect(healthCheckCannotExec(input)).toBe(false); + }); + } +}); + +describe("formatNotReadyReason", () => { + const unhealthy: ContainerState = { + running: true, + status: "running", + exitCode: 0, + oomKilled: false, + health: "unhealthy", + }; + + it("is the bare health status when Docker recorded no probe", () => { + expect(formatNotReadyReason({ ...unhealthy, health: "starting" })).toBe( + "container is not ready: starting", + ); + }); + + it("appends the last probe's exit code and output", () => { + expect( + formatNotReadyReason({ + ...unhealthy, + lastHealthProbe: { exitCode: -1, output: PROBE_TIMEOUT_OUTPUT }, + }), + ).toBe( + "container is not ready: unhealthy (last health check exit code -1: Health check exceeded timeout (2s))", + ); + }); + + it("appends only the exit code when the probe printed nothing", () => { + expect( + formatNotReadyReason({ ...unhealthy, lastHealthProbe: { exitCode: 1, output: "" } }), + ).toBe("container is not ready: unhealthy (last health check exit code 1)"); + }); +}); + describe("listVolumesByLabel", () => { it.live("returns volume names for a successful listing", () => { const mock = mockSpawner({ stdout: "supabase_db_my-app\n" }); 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 7da13328a6..efe631df6d 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/command-internal/edge-runtime-script.layer.integration.test.ts b/apps/cli/src/command-internal/edge-runtime-script.layer.integration.test.ts index 0ec04985c8..535e628a85 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 @@ -215,7 +215,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/command-internal/functions-local-config.ts b/apps/cli/src/command-internal/functions-local-config.ts index e70704c837..8ded3eed41 100644 --- a/apps/cli/src/command-internal/functions-local-config.ts +++ b/apps/cli/src/command-internal/functions-local-config.ts @@ -15,22 +15,18 @@ 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 functionsLocalConfigLoader: FunctionsLocalConfigLoader = { 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.resolvedConfig.loaded.document, - ), - catch: toError, - }); + const validated = yield* resolveLocalConfigValues( + context.config, + context.hostname, + projectRoot, + context.resolvedConfig.loaded.document, + ).pipe(Effect.mapError(toError)); return { loaded: { config: context.config, document: context.resolvedConfig.loaded.document }, resolvedConfig: context.resolvedConfig, diff --git a/apps/cli/src/command-internal/http-errors.ts b/apps/cli/src/command-internal/http-errors.ts index eb3f93a9c5..136945f5a8 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 @@ -119,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) { @@ -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/local-config-values.ts b/apps/cli/src/command-internal/local-config-values.ts index 5e881627ec..878e44541b 100644 --- a/apps/cli/src/command-internal/local-config-values.ts +++ b/apps/cli/src/command-internal/local-config-values.ts @@ -1,5 +1,3 @@ -import { readFileSync } from "node:fs"; - import type { CliConfig } from "@supabase/config"; import { DEFAULT_LOCAL_DATABASE_PASSWORD, @@ -13,7 +11,7 @@ import { defaultPublishableKey, defaultSecretKey, } from "../shared/stack-constants.ts"; -import { Effect, Option, Schema } from "effect"; +import { Data, Effect, FileSystem, Option, Path, Schema } from "effect"; import * as Base64Url from "effect/encoding/Base64Url"; import { CliConfigValueError } from "../config/cli-config.errors.ts"; @@ -43,6 +41,7 @@ import { type DbInput, emailContentPathReadErrorMessage, type ExperimentalInput, + fsErrorCause, type HookInput, type LocalSmtpInput, type MfaFactorInput, @@ -136,10 +135,10 @@ function apiUrlWithPath(apiExternalUrl: string, path: string): string { * Thrown 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 { @@ -150,6 +149,29 @@ export class InvalidJwtSecretError extends Error { /** Minimum `auth.jwt_secret` length. */ const MIN_JWT_SECRET_LENGTH = 16; +/** + * A failure that is not one of this module's config errors (a signing-key error, an unsupported + * algorithm), 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 ConfigValidateError || + cause instanceof InvalidJwtSecretError || + cause instanceof CliConfigValueError + ? cause + : cause instanceof Error + ? new LocalConfigValuesError(cause.message, cause.name) + : new LocalConfigValuesError(String(cause)); + +const attempt =
(thunk: () => A) => Effect.try({ try: thunk, catch: thrownError }); + /** Narrows a configured string to one of `allowed`, failing with the codec wording otherwise. */ export const narrowConfigEnum = ( path: string, @@ -261,36 +283,39 @@ 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; +}); /** * Returns the parsed signing keys only when auth is enabled and a path is configured; @@ -298,40 +323,46 @@ function readSigningKeysFile(workdir: string, signingKeysPath: string): Readonly * {@link resolveLocalJwks} and `start.handler.ts`'s `GOTRUE_JWT_KEYS` so both resolvers agree * on which key(s) apply. */ -export function resolveConfiguredSigningKeys( +export const resolveConfiguredSigningKeys = Effect.fnUntraced(function* ( config: CliConfig, workdir: string, -): ReadonlyArray | undefined { +) { const signingKeysPath = config.auth.signing_keys_path; return config.auth.enabled && signingKeysPath !== undefined && signingKeysPath.length > 0 - ? readSigningKeysFile(workdir, signingKeysPath) + ? yield* readSigningKeysFile(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. `subject` is `string | undefined` rather than a plain @@ -404,39 +435,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)), + ), + ), + ); } -} +}); /** The effective `db.settings`, never `undefined` so callers can read fields directly. */ export function resolveDbSettingsEnvOverrides( @@ -711,7 +764,7 @@ export function resolveAuthExternalProviders( * @throws {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, @@ -720,7 +773,7 @@ export function resolveLocalConfigValues( * (not the always-defaulted decoded value). `undefined` callers skip those checks. */ document?: Readonly>, -): LocalConfigValues { +) { const resolvedProjectId = config.project_id ?? ""; const apiTlsEnabled = config.api.tls.enabled; @@ -728,16 +781,18 @@ export function resolveLocalConfigValues( const apiTlsCertPath = config.api.tls.cert_path; const apiTlsKeyPath = config.api.tls.key_path; if (apiEnabled && apiTlsEnabled) { - readApiTlsFiles(workdir, apiTlsCertPath, apiTlsKeyPath); + yield* readApiTlsFiles(workdir, apiTlsCertPath, apiTlsKeyPath); } const apiPort = config.api.port; - const apiExternalUrl = resolveApiExternalUrl( - { - external_url: config.api.external_url, - port: apiPort, - tls: { enabled: apiTlsEnabled }, - }, - hostname, + const apiExternalUrl = yield* attempt(() => + resolveApiExternalUrl( + { + external_url: config.api.external_url, + port: apiPort, + tls: { enabled: apiTlsEnabled }, + }, + hostname, + ), ); const dbPort = config.db.port; const majorVersion = config.db.major_version; @@ -745,7 +800,7 @@ export function resolveLocalConfigValues( // The resolved value is written verbatim into `/etc/postgresql-custom/pgsodium_root.key`. const rawRootKey = asRecord(document?.["db"])?.["root_key"]; if (rawRootKey !== undefined && typeof rawRootKey !== "string") { - throw new ConfigValidateError("failed to parse config:\ndb.root_key: expected a table"); + return yield* new ConfigValidateError("failed to parse config:\ndb.root_key: expected a table"); } const rootKey = rawRootKey === undefined || rawRootKey.length === 0 ? POSTGRES_DEFAULT_ROOT_KEY : rawRootKey; @@ -756,7 +811,7 @@ export function resolveLocalConfigValues( const studioApiUrl = config.studio.api_url; const mailpitEnabled = config.local_smtp.enabled; const mailpitPort = config.local_smtp.port; - const jwtSecret = resolveJwtSecret(config.auth.jwt_secret); + const jwtSecret = yield* attempt(() => resolveJwtSecret(config.auth.jwt_secret)); const signingKeysPath = config.auth.signing_keys_path; // The signing-keys file read only runs when auth is enabled, so a disabled auth section // never opens/parses `signing_keys_path`, even a stale or missing one. JWT-secret validation @@ -769,7 +824,7 @@ export function resolveLocalConfigValues( // default key, not fall back to symmetric HS256. const signingKey = signingKeysPath !== undefined && signingKeysPath.length > 0 - ? (resolveConfiguredSigningKeys(config, workdir) ?? [DEFAULT_SIGNING_KEY])[0] + ? ((yield* resolveConfiguredSigningKeys(config, workdir)) ?? [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 @@ -779,7 +834,8 @@ export function resolveLocalConfigValues( const passkeyDoc = asRecord(authDocument?.["passkey"]); const webauthnDoc = asRecord(authDocument?.["webauthn"]); const passkeyEnabled = - passkeyDoc !== undefined && rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"); + passkeyDoc !== undefined && + (yield* attempt(() => rawUnmodeledBool(passkeyDoc["enabled"], "auth.passkey.enabled"))); const rpId = asString(webauthnDoc?.["rp_id"]); const rpOrigins = rawOrigins(webauthnDoc?.["rp_origins"]); const passkey: PasskeyInput | undefined = passkeyEnabled @@ -813,7 +869,7 @@ export function resolveLocalConfigValues( }, ]; - readAuthEmailTemplateContent(resolveAuthEmail(config.auth.email, authDocument), workdir); + yield* readAuthEmailTemplateContent(resolveAuthEmail(config.auth.email, authDocument), workdir); const resolvedSmtp = resolveAuthEmailSmtp(authDocument); const smtp: SmtpInput | undefined = @@ -835,17 +891,16 @@ export function resolveLocalConfigValues( hooks, mfa, smtp, - thirdParty: resolveThirdPartyProviders(config.auth.third_party), + thirdParty: yield* attempt(() => resolveThirdPartyProviders(config.auth.third_party)), }; } const functionSlugs = Object.keys(config.functions); const denoVersion = config.edge_runtime.deno_version; const analyticsEnabled = config.analytics.enabled; - const analyticsBackend = narrowConfigEnum("analytics.backend", config.analytics.backend, [ - "postgres", - "bigquery", - ]); + const analyticsBackend = yield* attempt(() => + narrowConfigEnum("analytics.backend", config.analytics.backend, ["postgres", "bigquery"]), + ); const gcpProjectId = config.analytics.gcp_project_id; const gcpProjectNumber = config.analytics.gcp_project_number; const gcpJwtPath = config.analytics.gcp_jwt_path; @@ -900,12 +955,23 @@ export function resolveLocalConfigValues( analytics: analyticsInput, experimental: experimentalInput, }; - validateResolvedConfig(input); + yield* attempt(() => validateResolvedConfig(input)); if (authEnabled) { - validateAuthSmsProviders(resolveAuthSms(config.auth.sms)); - validateAuthExternalProviders(resolveAuthExternalProviders(authDocument, config.auth.external)); + yield* attempt(() => validateAuthSmsProviders(resolveAuthSms(config.auth.sms))); + yield* attempt(() => + validateAuthExternalProviders( + resolveAuthExternalProviders(authDocument, config.auth.external), + ), + ); } + const anonKey = yield* attempt(() => + resolveSignedKey(config.auth.anon_key, jwtSecret, signingKey, "anon"), + ); + const serviceRoleKey = yield* attempt(() => + resolveSignedKey(config.auth.service_role_key, jwtSecret, signingKey, "service_role"), + ); + return { apiUrl: apiExternalUrl, apiPort, @@ -934,13 +1000,8 @@ export function resolveLocalConfigValues( publishableKey: resolveOpaqueKey(config.auth.publishable_key, defaultPublishableKey), secretKey: resolveOpaqueKey(config.auth.secret_key, defaultSecretKey), jwtSecret, - anonKey: resolveSignedKey(config.auth.anon_key, jwtSecret, signingKey, "anon"), - serviceRoleKey: resolveSignedKey( - config.auth.service_role_key, - jwtSecret, - signingKey, - "service_role", - ), + anonKey, + serviceRoleKey, storageS3Url: apiUrlWithPath(apiExternalUrl, "/storage/v1/s3"), storageS3AccessKeyId: DEFAULT_S3_ACCESS_KEY_ID, storageS3SecretAccessKey: DEFAULT_S3_SECRET_ACCESS_KEY, @@ -954,8 +1015,8 @@ export function resolveLocalConfigValues( // presence-only and must see the raw value to reject an explicit `project_id = ""`. projectId: sanitizeProjectId(resolvedProjectId), edgeRuntimeDenoVersion: denoVersion, - }; -} + } satisfies LocalConfigValues; +}); /** Resolves local and remote signing keys for services that consume the local JWKS document. */ export const resolveLocalJwks = Effect.fnUntraced(function* ( @@ -963,34 +1024,33 @@ export const resolveLocalJwks = Effect.fnUntraced(function* ( workdir: string, jwtSecret: string, ) { - const { issuerUrl, signingKeys, signingKeysPath } = yield* Effect.try({ - try: () => { - const signingKeysPath = config.auth.signing_keys_path; - // 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) ?? [ - DEFAULT_SIGNING_KEY, - ]; - - // `resolveThirdPartyIssuerUrl`'s "at most one enabled" + required-field checks are only - // meaningful while auth is enabled; when auth is disabled they are skipped, so the - // unchecked, no-throw issuer-url builder applies instead. - const thirdParty = config.auth.third_party; - const issuerUrl = Option.fromNullishOr( + const signingKeysPath = config.auth.signing_keys_path; + // 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, + )) ?? [DEFAULT_SIGNING_KEY]; + + // `resolveThirdPartyIssuerUrl`'s "at most one enabled" + required-field checks are only + // meaningful while auth is enabled; when auth is disabled they are skipped, so the + // unchecked, no-throw issuer-url builder applies instead. + const thirdParty = config.auth.third_party; + const issuerUrl = Option.fromNullishOr( + yield* Effect.try({ + try: () => config.auth.enabled ? 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)), - }); + catch: (cause) => + cause instanceof ConfigValidateError + ? cause + : new ConfigValidateError(cause instanceof Error ? cause.message : String(cause)), + }), + ).pipe(Option.filter((value) => value.length > 0)); 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 11b4087904..79353d4832 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,12 +1,11 @@ import { generateKeyPairSync } from "node:crypto"; -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { BunServices } from "@effect/platform-bun"; import { CliConfigSchema, type CliConfig } from "@supabase/config"; -import { Effect, Schema } from "effect"; +import { Effect, FileSystem, Path, Schema } from "effect"; import { FetchHttpClient } from "effect/http"; import { importJWK, jwtVerify } from "jose"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "@effect/vitest"; import { useTempWorkdir } from "../../tests/helpers/command-mocks.ts"; import { CliConfigValueError } from "../config/cli-config.errors.ts"; @@ -26,12 +25,28 @@ import { resolveLocalJwks, } from "./local-config-values.ts"; +const join = (...segments: ReadonlyArray) => + Effect.gen(function* () { + const path = yield* Path.Path; + return path.join(...segments); + }); + +const mkdirp = (directory: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.makeDirectory(directory, { recursive: true }); + }); + +const writeFile = (file: string, contents: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.writeFileString(file, contents); + }); + 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 decodeConfig = Schema.decodeUnknownSync(CliConfigSchema); @@ -48,208 +63,269 @@ 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 decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)); +const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); -describe("resolveLocalConfigValues", () => { - it("derives every URL from api.external_url when unset", () => { - const config = baseConfig(); - const values = 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"); - expect(values.graphqlUrl).toBe("http://127.0.0.1:54321/graphql/v1"); - expect(values.functionsUrl).toBe("http://127.0.0.1:54321/functions/v1"); - expect(values.mcpUrl).toBe("http://127.0.0.1:54321/mcp"); - expect(values.storageS3Url).toBe("http://127.0.0.1:54321/storage/v1/s3"); - expect(values.studioUrl).toBe("http://127.0.0.1:54323"); - expect(values.mailpitUrl).toBe("http://127.0.0.1:54324"); +const writeSigningKeys = (workdir: string, jwks: ReadonlyArray>) => + Effect.gen(function* () { + const supabaseDir = yield* join(workdir, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* join(supabaseDir, "signing_keys.json"), yield* encodeJson(jwks)); }); - 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); - expect(values.apiUrl).toBe("https://127.0.0.1:54321"); - }); +describe("resolveLocalConfigValues", () => { + it.live("derives every URL from api.external_url when unset", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* 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"); + expect(values.graphqlUrl).toBe("http://127.0.0.1:54321/graphql/v1"); + expect(values.functionsUrl).toBe("http://127.0.0.1:54321/functions/v1"); + expect(values.mcpUrl).toBe("http://127.0.0.1:54321/mcp"); + expect(values.storageS3Url).toBe("http://127.0.0.1:54321/storage/v1/s3"); + expect(values.studioUrl).toBe("http://127.0.0.1:54323"); + expect(values.mailpitUrl).toBe("http://127.0.0.1:54324"); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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); - expect(values.apiUrl).toBe("https://example.test"); - expect(values.restUrl).toBe("https://example.test/rest/v1"); - }); + it.live("uses https and the configured port when api.tls.enabled", () => + Effect.gen(function* () { + const config = baseConfig({ api: { tls: { enabled: true }, port: 54321 } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://127.0.0.1:54321"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("brackets an IPv6 hostname when building host:port", () => { - const config = baseConfig(); - const values = resolveLocalConfigValues(config, "::1", WORKDIR); - expect(values.apiUrl).toBe("http://[::1]:54321"); - }); + it.live("uses api.external_url verbatim when configured", () => + Effect.gen(function* () { + const config = baseConfig({ api: { external_url: "https://example.test" } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.apiUrl).toBe("https://example.test"); + expect(values.restUrl).toBe("https://example.test/rest/v1"); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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); - expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); - }); + it.live("brackets an IPv6 hostname when building host:port", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "::1", WORKDIR); + expect(values.apiUrl).toBe("http://[::1]:54321"); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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); - 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"); - }); + it.live("builds the db URL with the hardcoded postgres password", () => + Effect.gen(function* () { + const config = baseConfig({ db: { port: 54322 } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.dbUrl).toBe("postgresql://postgres:postgres@127.0.0.1:54322/postgres"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("uses configured opaque keys verbatim when set", () => { - const config = baseConfig({ - auth: { publishable_key: "sb_publishable_custom", secret_key: "sb_secret_custom" }, - }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); - expect(values.publishableKey).toBe("sb_publishable_custom"); - expect(values.secretKey).toBe("sb_secret_custom"); - }); + it.live("falls back to the default JWT secret and opaque keys when unset", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* 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"); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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 [, anonPayload] = values.anonKey.split("."); - const [, serviceRolePayload] = values.serviceRoleKey.split("."); - expect(JSON.parse(Buffer.from(anonPayload ?? "", "base64url").toString())).toMatchObject({ - role: "anon", - }); - expect(JSON.parse(Buffer.from(serviceRolePayload ?? "", "base64url").toString())).toMatchObject( - { role: "service_role" }, - ); - }); + it.live("uses configured opaque keys verbatim when set", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { publishable_key: "sb_publishable_custom", secret_key: "sb_secret_custom" }, + }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.publishableKey).toBe("sb_publishable_custom"); + expect(values.secretKey).toBe("sb_secret_custom"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("uses configured anon/service_role keys verbatim when set", () => { - const config = baseConfig({ - auth: { anon_key: "configured-anon", service_role_key: "configured-service-role" }, - }); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); - expect(values.anonKey).toBe("configured-anon"); - expect(values.serviceRoleKey).toBe("configured-service-role"); - }); + it.live("signs the default anon/service_role JWTs from the resolved secret", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + const [, anonPayload] = values.anonKey.split("."); + const [, serviceRolePayload] = values.serviceRoleKey.split("."); + expect( + yield* decodeJson(Buffer.from(anonPayload ?? "", "base64url").toString()), + ).toMatchObject({ + role: "anon", + }); + expect( + yield* decodeJson(Buffer.from(serviceRolePayload ?? "", "base64url").toString()), + ).toMatchObject({ role: "service_role" }); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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); - expect(values.jwtSecret).toBe("a".repeat(32)); - expect(values.anonKey).not.toBe(""); - }); + it.live("uses configured anon/service_role keys verbatim when set", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { anon_key: "configured-anon", service_role_key: "configured-service-role" }, + }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.anonKey).toBe("configured-anon"); + expect(values.serviceRoleKey).toBe("configured-service-role"); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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( - InvalidJwtSecretError, - ); - }); + it.live("signs anon/service_role JWTs from a configured jwt_secret", () => + Effect.gen(function* () { + const config = baseConfig({ auth: { jwt_secret: "a".repeat(32) } }); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.jwtSecret).toBe("a".repeat(32)); + expect(values.anonKey).not.toBe(""); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("rejects an explicit empty project_id", () => { - // 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( - "Missing required field in config: project_id", - ); - }); + it.live("rejects a configured jwt_secret shorter than 16 characters", () => + Effect.gen(function* () { + const config = baseConfig({ auth: { jwt_secret: "a".repeat(15) } }); + expect( + yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)), + ).toBeInstanceOf(InvalidJwtSecretError); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("hardcodes the local S3 credentials", () => { - const config = baseConfig(); - const values = resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); - expect(values.storageS3AccessKeyId).toBe("625729a08b95bf1b7ff351a663f3a23c"); - expect(values.storageS3SecretAccessKey).toBe( - "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", - ); - expect(values.storageS3Region).toBe("local"); - }); + it.live("rejects an explicit empty project_id", () => + Effect.gen(function* () { + // An explicit `project_id = ""` overwrites the workdir-basename default with the literal + // empty string, unlike an absent key. + const config = baseConfig({ project_id: "" }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain("Missing required field in config: project_id"); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.live("hardcodes the local S3 credentials", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.storageS3AccessKeyId).toBe("625729a08b95bf1b7ff351a663f3a23c"); + expect(values.storageS3SecretAccessKey).toBe( + "850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907", + ); + expect(values.storageS3Region).toBe("local"); + }).pipe(Effect.provide(BunServices.layer)), + ); describe("non-auth port validation", () => { // 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. // api.enabled defaults to true, so this rejection applies without an explicit // `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( - "Missing required field in config: api.port", - ); - }); + it.live("rejects a configured api.port of 0 when api is enabled", () => + Effect.gen(function* () { + const config = baseConfig({ api: { port: 0 } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain("Missing required field in config: api.port"); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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(); - }); + it.live("does not reject a zero api.port when api is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ api: { enabled: false, port: 0 } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); // 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( - "Missing required field in config: studio.port", - ); - }); + it.live("rejects a configured studio.port of 0 when studio is enabled", () => + Effect.gen(function* () { + const config = baseConfig({ studio: { port: 0 } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain("Missing required field in config: studio.port"); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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(); - }); + it.live("does not reject a zero studio.port when studio is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ studio: { enabled: false, port: 0 } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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( - `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, - ); - }); + it.live( + "rejects a malformed studio.api_url (unterminated IPv6 literal) when studio is enabled", + () => + Effect.gen(function* () { + const config = baseConfig({ studio: { api_url: "http://[::1" } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain( + `Invalid config for studio.api_url: parse "http://[::1": missing ']' in host`, + ); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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(); - }); + it.live("does not reject a malformed studio.api_url when studio is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ studio: { enabled: false, api_url: "http://[::1" } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("does not throw for the default studio.api_url", () => { - const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); - }); + it.live("does not throw for the default studio.api_url", () => + Effect.gen(function* () { + const config = baseConfig(); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); // local_smtp.enabled defaults to true, so this rejection applies without an explicit // `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( - "Missing required field in config: local_smtp.port", - ); - }); + it.live("rejects a configured local_smtp.port of 0 when local_smtp is enabled", () => + Effect.gen(function* () { + const config = baseConfig({ local_smtp: { port: 0 } }); + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR))).message, + ).toContain("Missing required field in config: local_smtp.port"); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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(); - }); + it.live("does not reject a zero local_smtp.port when local_smtp is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ local_smtp: { enabled: false, port: 0 } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); }); 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); - expect(values.rootKey).toBe(POSTGRES_DEFAULT_ROOT_KEY); - }); + it.live("falls back to the default root key when absent", () => + Effect.gen(function* () { + const config = baseConfig(); + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR); + expect(values.rootKey).toBe(POSTGRES_DEFAULT_ROOT_KEY); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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, document); - expect(values.rootKey).toBe("custom-root-key"); - }); + it.live("uses a configured string root_key verbatim", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { db: { root_key: "custom-root-key" } }; + const values = yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document); + expect(values.rootKey).toBe("custom-root-key"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("rejects a non-string root_key (e.g. a bare TOML integer)", () => { - const config = baseConfig(); - const document = { db: { root_key: 12345 } }; - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document)).toThrow( - "failed to parse config:\ndb.root_key: expected a table", - ); - }); + it.live("rejects a non-string root_key (e.g. a bare TOML integer)", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { db: { root_key: 12345 } }; + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document))) + .message, + ).toContain("failed to parse config:\ndb.root_key: expected a table"); + }).pipe(Effect.provide(BunServices.layer)), + ); }); // storage.buckets validation lives entirely in config-validate.unit.test.ts; this file has no @@ -258,10 +334,14 @@ describe("resolveLocalConfigValues", () => { describe("experimental.* (experimental.validate())", () => { // Exercises this function's own fallback when no `document` (5th param) is supplied; the // 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(); - }); + it.live( + "does not throw a present [experimental.webhooks] section without enabled when no document is provided", + () => + Effect.gen(function* () { + const config = baseConfig({ experimental: { webhooks: {} } }); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); }); describe("resolveAuthCaptcha", () => { @@ -415,113 +495,151 @@ 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); - - 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 = await jwtVerify(values.serviceRoleKey, publicKey); - expect(serviceRole.payload).toMatchObject({ role: "service_role" }); - }); + 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 serviceRole = yield* Effect.promise(() => + jwtVerify(values.serviceRoleKey, publicKey), + ); + expect(serviceRole.payload).toMatchObject({ role: "service_role" }); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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("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(BunServices.layer)), + ); - 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("uses an absolute signing_keys_path as-is, without joining the workdir", () => + Effect.gen(function* () { + const jwk = generateRsaJwk(); + yield* writeSigningKeys(tempRoot.current, [jwk]); + const absolutePath = yield* 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(BunServices.layer)), + ); - 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("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(BunServices.layer)), + ); - 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("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(BunServices.layer)), + ); - it("throws an 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 an 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(BunServices.layer)), + ); - it("throws an 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("throws an error when the signing keys file is malformed JSON", () => + Effect.gen(function* () { + const supabaseDir = yield* join(tempRoot.current, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* 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(BunServices.layer)), + ); - 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(BunServices.layer)), + ); - 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(); - }); + it.live("skips reading a missing signing_keys_path when auth is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { enabled: false, signing_keys_path: "missing.json" }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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 supabaseDir = yield* join(tempRoot.current, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* 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(BunServices.layer)), + ); }); // Required-field/range assertions live in config-validate.unit.test.ts; only env-override @@ -531,141 +649,186 @@ 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`. - 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, document)).toThrow( - '"not-a-bool" is not a valid boolean', - ); - }); + it.live( + "throws on an unparsable raw auth.passkey.enabled string instead of silently disabling it", + () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { auth: { passkey: { enabled: "not-a-bool" } } }; + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document))) + .message, + ).toContain('"not-a-bool" is not a valid boolean'); + }).pipe(Effect.provide(BunServices.layer)), + ); }); 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(BunServices.layer)), + ); - 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(BunServices.layer)), + ); - 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* () { + yield* writeFile(yield* join(tempRoot.current, "invite.html"), ""); + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: { content_path: "invite.html" } } }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("does not throw a template with no content_path configured", () => { - 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.live("does not throw a template with no content_path configured", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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(BunServices.layer)), + ); - 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 templateDir = yield* join(tempRoot.current, "supabase", "templates"); + yield* mkdirp(templateDir); + yield* writeFile(yield* 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(); - }); + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("does not throw a disabled notification's missing content_path", () => { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { - notification: { - password_changed: { enabled: false, content_path: "missing.html" }, + it.live("does not throw a disabled notification's missing content_path", () => + Effect.gen(function* () { + 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)).not.toThrow(); - }); + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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(); - }); + it.live("does not throw a missing template content_path when auth is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: false, + email: { template: { invite: { content_path: "missing.html" } } }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("rejects a template content key present without content_path", () => { - const config = baseConfig({ - auth: { - enabled: true, - site_url: "http://localhost:3000", - email: { template: { invite: {} } }, - }, - }); - expect(() => - resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current, { - auth: { email: { template: { invite: { content: "Hi" } } } }, - }), - ).toThrow( - "Invalid config for auth.email.template.invite.content: please use content_path instead", - ); - }); + it.live("rejects a template content key present without content_path", () => + Effect.gen(function* () { + const config = baseConfig({ + auth: { + enabled: true, + site_url: "http://localhost:3000", + email: { template: { invite: {} } }, + }, + }); + expect( + (yield* Effect.flip( + resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current, { + auth: { email: { template: { invite: { content: "Hi" } } } }, + }), + )).message, + ).toContain( + "Invalid config for auth.email.template.invite.content: please use content_path instead", + ); + }).pipe(Effect.provide(BunServices.layer)), + ); }); // Required-field/range assertions live in config-validate.unit.test.ts; only env-override @@ -674,42 +837,58 @@ describe("resolveLocalConfigValues", () => { describe("auth.external (external.validate(), D-only, ported to L)", () => { // Unmodeled external providers are silently dropped by the decoded config, so this reads the // raw `document` (5th param) instead. - it("rejects an enabled unmodeled external provider missing client_id", () => { - const config = baseConfig(); - const document = { auth: { external: { custom: { enabled: true } } } }; - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document)).toThrow( - "Missing required field in config: auth.external.custom.client_id", - ); - }); + it.live("rejects an enabled unmodeled external provider missing client_id", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { auth: { external: { custom: { enabled: true } } } }; + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document))) + .message, + ).toContain("Missing required field in config: auth.external.custom.client_id"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("rejects an enabled unmodeled external provider missing secret", () => { - const config = baseConfig(); - const document = { - auth: { external: { custom: { enabled: true, client_id: "abc" } } }, - }; - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document)).toThrow( - "Missing required field in config: auth.external.custom.secret", - ); - }); + it.live("rejects an enabled unmodeled external provider missing secret", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { + auth: { external: { custom: { enabled: true, client_id: "abc" } } }, + }; + expect( + (yield* Effect.flip(resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document))) + .message, + ).toContain("Missing required field in config: auth.external.custom.secret"); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("does not require a secret for apple/google providers", () => { - const config = baseConfig(); - const document = { - auth: { external: { apple: { enabled: true, client_id: "abc" } } }, - }; - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document)).not.toThrow(); - }); + it.live("does not require a secret for apple/google providers", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { + auth: { external: { apple: { enabled: true, client_id: "abc" } } }, + }; + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("does not validate a disabled unmodeled external provider", () => { - const config = baseConfig(); - const document = { auth: { external: { custom: { enabled: false } } } }; - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document)).not.toThrow(); - }); + it.live("does not validate a disabled unmodeled external provider", () => + Effect.gen(function* () { + const config = baseConfig(); + const document = { auth: { external: { custom: { enabled: false } } } }; + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - it("skips the check entirely when no document is threaded through", () => { - const config = baseConfig(); - expect(() => resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).not.toThrow(); - }); + it.live("skips the check entirely when no document is threaded through", () => + Effect.gen(function* () { + const config = baseConfig(); + expect(yield* resolveLocalConfigValues(config, "127.0.0.1", WORKDIR)).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); }); describe("resolveAuthSms (top-level scalars)", () => { @@ -752,70 +931,95 @@ 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 = (workdir: string, name: string, contents = "dummy") => + Effect.gen(function* () { + const supabaseDir = yield* join(workdir, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* 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(); - }); + it.live("does not throw when tls.enabled with neither cert_path nor key_path set", () => + Effect.gen(function* () { + const config = baseConfig({ api: { tls: { enabled: true } } }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); // 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 an 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 an 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(BunServices.layer)), + ); - it("throws an 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 an 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(BunServices.layer)), + ); - 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" } }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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", + }, + }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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(); - }); + it.live("skips TLS validation entirely when api is disabled", () => + Effect.gen(function* () { + const config = baseConfig({ + api: { enabled: false, tls: { enabled: true, cert_path: "missing-cert.pem" } }, + }); + expect( + yield* resolveLocalConfigValues(config, "127.0.0.1", tempRoot.current), + ).toBeDefined(); + }).pipe(Effect.provide(BunServices.layer)), + ); }); }); @@ -838,217 +1042,267 @@ describe("narrowConfigEnum", () => { 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(BunServices.layer)), + ); - 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> }; + + 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(BunServices.layer)), + ); - 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> }; + 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> }; - expect(parsed.keys[0]).toMatchObject({ use: "sig", ext: true, key_ops: ["verify"] }); - }); + expect(parsed.keys[0]).toMatchObject({ use: "sig", ext: true, key_ops: ["verify"] }); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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", - }, - ], - }); - }); + 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(BunServices.layer)), + ); - it("throws an 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("throws an 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(BunServices.layer)), + ); - it("throws an 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 an error when the signing keys file is malformed JSON", () => + Effect.gen(function* () { + const supabaseDir = yield* join(tempRoot.current, "supabase"); + yield* mkdirp(supabaseDir); + yield* writeFile(yield* 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(BunServices.layer)), + ); 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(BunServices.layer)), + ); - 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(BunServices.layer)), + ); - it("does not validate third-party providers when auth is disabled", 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" }, - }); - } - if (url === `${issuerUrl}/jwks.json`) { - return new Response(JSON.stringify({ keys: remoteKeys }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - 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" }, + it.live("does not validate third-party providers when auth is disabled", () => + 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}`)); + }); + 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(); - }); - - it("does not attempt a remote JWKS fetch for an enabled third-party provider with an empty issuer_url", async () => { - 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 }; - - expect(fetchMock).not.toHaveBeenCalled(); - expect(jwks.keys.length).toBeGreaterThan(0); - 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(BunServices.layer)), + ); - 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" }, + it.live( + "does not attempt a remote JWKS fetch for an enabled third-party provider with an empty issuer_url", + () => + Effect.gen(function* () { + const fetchMock = vi.spyOn(globalThis, "fetch"); + const config = baseConfig({ + auth: { + enabled: false, + third_party: { workos: { enabled: true, issuer_url: "" } }, + }, }); - } - throw 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 jwksJson = yield* runLocalJwks(config, WORKDIR, "a".repeat(32)); + const jwks = (yield* decodeJson(jwksJson)) as { keys: ReadonlyArray }; - expect(parsed.keys).toEqual( - expect.arrayContaining([expect.objectContaining({ kid: "remote-key" })]), - ); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); + expect(fetchMock).not.toHaveBeenCalled(); + expect(jwks.keys.length).toBeGreaterThan(0); + fetchMock.mockRestore(); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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("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 = 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); + }).pipe(Effect.provide(BunServices.layer)), + ); - 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", - ); - }); + 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" } }, + }, + }); + 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(BunServices.layer)), + ); }); }); diff --git a/apps/cli/src/command-internal/migration-apply.ts b/apps/cli/src/command-internal/migration-apply.ts index 9c69929a0e..40eb63121c 100644 --- a/apps/cli/src/command-internal/migration-apply.ts +++ b/apps/cli/src/command-internal/migration-apply.ts @@ -283,15 +283,15 @@ export const checkScannerBufferSize = ( /** * Renders a `^` caret line under the error position of a failing statement. `pos` is the - * server's 1-based error cursor, measured in UTF-8 bytes, so multibyte statements shift the caret - * correctly. The caret line replaces every line after the error line. Position 0 (absent), a + * server's 1-based error cursor, measured in characters (code points), not UTF-8 bytes or UTF-16 + * units. The caret line replaces every line after the error line. Position 0 (absent), a * position past the end of the statement, or one landing exactly on a line break leave the * statement untouched. */ export const markError = (stat: string, pos: number): string => { const lines = stat.split("\n"); for (const [j, line] of lines.entries()) { - const c = utf8ByteLength(line); + const c = [...line].length; if (pos > c) { pos -= c + 1; continue; diff --git a/apps/cli/src/command-internal/migration-apply.unit.test.ts b/apps/cli/src/command-internal/migration-apply.unit.test.ts index 001178fdc4..224e8f2618 100644 --- a/apps/cli/src/command-internal/migration-apply.unit.test.ts +++ b/apps/cli/src/command-internal/migration-apply.unit.test.ts @@ -834,10 +834,22 @@ describe("markError", () => { expect(markError("abcde", 5)).toBe("abcde\n ^"); }); - it("consumes the position in UTF-8 bytes, not characters", () => { - expect(markError("héllo\nworld", 8)).toBe("héllo\nworld\n^"); + it("consumes the position in characters, not UTF-8 bytes", () => { + expect(markError("héllo\nworld", 7)).toBe("héllo\nworld\n^"); expect(markError("sélect 1", 3)).toBe("sélect 1\n ^"); }); + + it("places the caret under the error after multibyte text on an earlier line", () => { + expect(markError("insert into notes (body) values ('éééééééé'),\n (foo bar);", 54)).toBe( + "insert into notes (body) values ('éééééééé'),\n (foo bar);\n ^", + ); + }); + + it("counts astral characters as one code point, not two UTF-16 units", () => { + expect(markError("insert into notes (body) values ('😀😀'),\n (foo bar);", 48)).toBe( + "insert into notes (body) values ('😀😀'),\n (foo bar);\n ^", + ); + }); }); describe("isPipelineIncompatible", () => { 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/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 66e4de49b4..6b248ce3a1 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'; diff --git a/apps/cli/src/command-internal/removed-command.ts b/apps/cli/src/command-internal/removed-command.ts index 898dfbbbd2..563a2df1a8 100644 --- a/apps/cli/src/command-internal/removed-command.ts +++ b/apps/cli/src/command-internal/removed-command.ts @@ -41,13 +41,11 @@ export const removedCommand = (suggestion: string) => return yield* Effect.gen(function* () { const commandRuntime = yield* CommandRuntime; const command = getCommandRuntimeCommand(commandRuntime); - return yield* Effect.fail( - new RemovedSurfaceError({ - message: `supabase ${command} was removed.`, - suggestion, - kind: "command", - }), - ); + return yield* new RemovedSurfaceError({ + message: `supabase ${command} was removed.`, + suggestion, + kind: "command", + }); }).pipe(Effect.ensuring(telemetryState.flush)); }); diff --git a/apps/cli/src/command-internal/removed-command.unit.test.ts b/apps/cli/src/command-internal/removed-command.unit.test.ts index cd8106beb1..b5d2935726 100644 --- a/apps/cli/src/command-internal/removed-command.unit.test.ts +++ b/apps/cli/src/command-internal/removed-command.unit.test.ts @@ -66,9 +66,11 @@ describe("TelemetryState flush", () => { }), ), Effect.provide( - commandRuntimeLayer(["db", "branch", "create"]).pipe(Layer.provide(BunCrypto.layer)), + Layer.mergeAll( + commandRuntimeLayer(["db", "branch", "create"]).pipe(Layer.provide(BunCrypto.layer)), + telemetry.layer, + ), ), - Effect.provide(telemetry.layer), ); }); diff --git a/apps/cli/src/command-internal/resolved-config-context.unit.test.ts b/apps/cli/src/command-internal/resolved-config-context.unit.test.ts index a92a544a2f..893c5dc73d 100644 --- a/apps/cli/src/command-internal/resolved-config-context.unit.test.ts +++ b/apps/cli/src/command-internal/resolved-config-context.unit.test.ts @@ -1,8 +1,6 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Effect, Layer } from "effect"; +import { Effect, FileSystem, Layer, Path } from "effect"; import { useTempWorkdir } from "../../tests/helpers/command-mocks.ts"; import { mockOutput, processEnvLayer } from "../../tests/helpers/mocks.ts"; @@ -13,11 +11,14 @@ import { loadLocalResolvedConfigContext } from "./resolved-config-context.ts"; const tempRoot = useTempWorkdir("supabase-resolved-config-context-"); -function writeProjectEnv(workdir: string, contents: string): void { - const supabaseDir = join(workdir, "supabase"); - mkdirSync(supabaseDir, { recursive: true }); - writeFileSync(join(supabaseDir, ".env"), contents); -} +const writeProjectEnv = (workdir: string, contents: string) => + Effect.gen(function* () { + 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, ".env"), contents); + }).pipe(Effect.provide(BunServices.layer)); /** Runs against a shell environment of exactly `env`, with the real config service and an empty Docker config dir. */ const layerWithShellEnv = (workdir: string, env: Readonly> = {}) => @@ -34,56 +35,64 @@ const layerWithShellEnv = (workdir: string, env: Readonly ), ), ), - processEnvLayer({ DOCKER_CONFIG: join(workdir, "docker-config"), ...env }), + processEnvLayer({ DOCKER_CONFIG: `${workdir}/docker-config`, ...env }), ), ); describe("loadLocalResolvedConfigContext hostname", () => { - it.effect("reads SUPABASE_SERVICES_HOSTNAME from a project .env", () => { - const workdir = tempRoot.current; - writeProjectEnv(workdir, "SUPABASE_SERVICES_HOSTNAME=dotenv-host\n"); + it.effect("reads SUPABASE_SERVICES_HOSTNAME from a project .env", () => + Effect.gen(function* () { + const workdir = tempRoot.current; + yield* writeProjectEnv(workdir, "SUPABASE_SERVICES_HOSTNAME=dotenv-host\n"); - return loadLocalResolvedConfigContext(workdir).pipe( - Effect.map((context) => { - expect(context.hostname).toBe("dotenv-host"); - }), - Effect.provide(layerWithShellEnv(workdir)), - ); - }); + return yield* loadLocalResolvedConfigContext(workdir).pipe( + Effect.map((context) => { + expect(context.hostname).toBe("dotenv-host"); + }), + Effect.provide(layerWithShellEnv(workdir)), + ); + }), + ); - it.effect("prefers a shell SUPABASE_SERVICES_HOSTNAME over a project .env", () => { - const workdir = tempRoot.current; - writeProjectEnv(workdir, "SUPABASE_SERVICES_HOSTNAME=dotenv-host\n"); + it.effect("prefers a shell SUPABASE_SERVICES_HOSTNAME over a project .env", () => + Effect.gen(function* () { + const workdir = tempRoot.current; + yield* writeProjectEnv(workdir, "SUPABASE_SERVICES_HOSTNAME=dotenv-host\n"); - return loadLocalResolvedConfigContext(workdir).pipe( - Effect.map((context) => { - expect(context.hostname).toBe("shell-host"); - }), - Effect.provide(layerWithShellEnv(workdir, { SUPABASE_SERVICES_HOSTNAME: "shell-host" })), - ); - }); + return yield* loadLocalResolvedConfigContext(workdir).pipe( + Effect.map((context) => { + expect(context.hostname).toBe("shell-host"); + }), + Effect.provide(layerWithShellEnv(workdir, { SUPABASE_SERVICES_HOSTNAME: "shell-host" })), + ); + }), + ); - it.effect("ignores a project .env DOCKER_HOST when discovering the daemon", () => { - const workdir = tempRoot.current; - writeProjectEnv(workdir, "DOCKER_HOST=tcp://x:2375\n"); + it.effect("ignores a project .env DOCKER_HOST when discovering the daemon", () => + Effect.gen(function* () { + const workdir = tempRoot.current; + yield* writeProjectEnv(workdir, "DOCKER_HOST=tcp://x:2375\n"); - return loadLocalResolvedConfigContext(workdir).pipe( - Effect.map((context) => { - expect(context.hostname).toBe("127.0.0.1"); - }), - Effect.provide(layerWithShellEnv(workdir)), - ); - }); + return yield* loadLocalResolvedConfigContext(workdir).pipe( + Effect.map((context) => { + expect(context.hostname).toBe("127.0.0.1"); + }), + Effect.provide(layerWithShellEnv(workdir)), + ); + }), + ); - it.effect("takes the daemon host from a shell DOCKER_HOST", () => { - const workdir = tempRoot.current; - writeProjectEnv(workdir, "DOCKER_HOST=tcp://project-dotenv-host:2375\n"); + it.effect("takes the daemon host from a shell DOCKER_HOST", () => + Effect.gen(function* () { + const workdir = tempRoot.current; + yield* writeProjectEnv(workdir, "DOCKER_HOST=tcp://project-dotenv-host:2375\n"); - return loadLocalResolvedConfigContext(workdir).pipe( - Effect.map((context) => { - expect(context.hostname).toBe("shell-daemon-host"); - }), - Effect.provide(layerWithShellEnv(workdir, { DOCKER_HOST: "tcp://shell-daemon-host:2375" })), - ); - }); + return yield* loadLocalResolvedConfigContext(workdir).pipe( + Effect.map((context) => { + expect(context.hostname).toBe("shell-daemon-host"); + }), + Effect.provide(layerWithShellEnv(workdir, { DOCKER_HOST: "tcp://shell-daemon-host:2375" })), + ); + }), + ); }); diff --git a/apps/cli/src/command-internal/stack-config.ts b/apps/cli/src/command-internal/stack-config.ts index d1a608c6fe..2f89f78f36 100644 --- a/apps/cli/src/command-internal/stack-config.ts +++ b/apps/cli/src/command-internal/stack-config.ts @@ -338,26 +338,32 @@ export const loadStackConfig = Effect.fn("StackConfig.load")( message: cause instanceof Error ? cause.message : String(cause), }), }); + const stackKeyError = (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 : value; - 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, - ); - 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 = (value: string | undefined) => + value === undefined || value === "" ? undefined : value; + 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 = yield* resolveConfiguredSigningKeys( + validatedConfig, + projectRoot, + ); + 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 @@ -378,13 +384,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: stackKeyError, + }); + }).pipe( + Effect.mapError(stackKeyError), + Effect.provideService(FileSystem.FileSystem, yield* FileSystem.FileSystem), + Effect.provideService(Path.Path, path), + ), ); const remoteJwks = Effect.gen(function* () { return issuer === undefined diff --git a/apps/cli/src/command-internal/status-values.ts b/apps/cli/src/command-internal/status-values.ts index e63c79fe31..530e8146a9 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"; @@ -203,7 +204,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 @@ -222,7 +223,7 @@ 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. `config` must be * the resolved config's materialized config so env overrides are already applied to every gate. @@ -231,7 +232,7 @@ export interface StatusLocalState { * @throws when `auth.signing_keys_path` is set but the file is missing, malformed, or its * first key is unsupported — see {@link generateAsymmetricLocalJwt}. */ -export function resolveStatusLocalState( +export const resolveStatusLocalState = Effect.fnUntraced(function* ( config: CliConfig, hostname: string, workdir: string, @@ -247,8 +248,9 @@ export function resolveStatusLocalState( * containers. */ precomputedLocal?: LocalConfigValues, -): StatusLocalState { - const local = precomputedLocal ?? resolveLocalConfigValues(config, hostname, workdir, document); +) { + const local = + precomputedLocal ?? (yield* resolveLocalConfigValues(config, hostname, workdir, document)); const apiEnabled = config.api.enabled; const studioSectionEnabled = config.studio.enabled; @@ -268,14 +270,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( @@ -380,7 +382,7 @@ export function statusValuesFromState( * 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, @@ -390,8 +392,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 88737edcd4..e4cb1956e3 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("emits MAILPIT_URL and no INBUCKET_URL 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).toBeUndefined(); @@ -354,30 +326,29 @@ describe("statusValues", () => { it("omits MAILPIT_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(); }); it("omits MAILPIT_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 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(); }); @@ -385,13 +356,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(); @@ -401,18 +367,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(); }); @@ -420,20 +390,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(); }); @@ -442,13 +409,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"); @@ -459,13 +421,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(); @@ -475,13 +432,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(); }); @@ -492,13 +444,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(); @@ -512,13 +459,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(); @@ -550,14 +492,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/target/resolve-target.integration.test.ts b/apps/cli/src/command-internal/target/resolve-target.integration.test.ts index 871225917d..0406d717f4 100644 --- a/apps/cli/src/command-internal/target/resolve-target.integration.test.ts +++ b/apps/cli/src/command-internal/target/resolve-target.integration.test.ts @@ -1,6 +1,17 @@ import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Cause, Effect, Exit, FileSystem, Layer, Option, Path, Redacted, Tracer } from "effect"; +import { + Cause, + Effect, + Exit, + FileSystem, + Layer, + Option, + Path, + Redacted, + Schema, + Tracer, +} from "effect"; import { VALID_REF, @@ -36,6 +47,8 @@ import { TargetPolicy } from "./target-policy.ts"; import type { TargetGroup } from "./target-policy.ts"; import type { TargetFlagValues } from "./target-flags.ts"; +const stringifyJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); + const none: TargetFlagValues = { remote: Option.none(), local: false, @@ -125,7 +138,7 @@ const run = ( if (options.cache !== undefined) { yield* fs.writeFileString( path.join(dir, "supabase", ".temp", "linked-project.json"), - JSON.stringify(options.cache), + stringifyJson(options.cache), ); } const resolver = projectRefResolver({ linked: options.linked ?? true }); @@ -197,7 +210,7 @@ const run = ( }).pipe(Effect.scoped, Effect.provide(BunServices.layer)); const succeeded = (exit: Exit.Exit): A => { - if (!Exit.isSuccess(exit)) throw new Error(`expected success, got ${JSON.stringify(exit)}`); + if (!Exit.isSuccess(exit)) throw new Error(`expected success, got ${stringifyJson(exit)}`); return exit.value; }; @@ -265,7 +278,7 @@ describe("resolveTarget", () => { const target = succeeded(result.exit); expect(target.kind).toBe("db-url"); if (target.kind === "db-url") expect(Redacted.value(target.url)).toBe(SECRET_URL); - expect(JSON.stringify(target)).not.toContain("hunter2"); + expect(stringifyJson(target)).not.toContain("hunter2"); expect(result.refFlags).toEqual([]); expect(result.apiRequests).toEqual([]); }), @@ -799,7 +812,7 @@ describe("resolveTarget", () => { _tag: "TargetRemoteAmbiguousError", message: `Can't tell whether the word after --remote is a branch (listing the branches of project ${FILE_REF} failed: HTTP 403). For the branch: supabase secrets unset --remote=. For the linked project, put --remote last: supabase secrets unset --remote`, }); - expect(JSON.stringify(error)).not.toContain("sk_live_secret"); + expect(stringifyJson(error)).not.toContain("sk_live_secret"); }), ); diff --git a/apps/cli/src/command-internal/target/target-flags.unit.test.ts b/apps/cli/src/command-internal/target/target-flags.unit.test.ts index b0d6f00406..7a2fa878fd 100644 --- a/apps/cli/src/command-internal/target/target-flags.unit.test.ts +++ b/apps/cli/src/command-internal/target/target-flags.unit.test.ts @@ -23,25 +23,32 @@ const silentFormatter: CliOutput.Formatter = { formatVersion: () => "", }; -const parseWith = ( +const runParse = ( config: Config, args: ReadonlyArray, + parsed: Array, ) => { - const parsed: Array = []; const command = Command.make("target-test", config).pipe( Command.withHandler((flags) => Effect.sync(() => parsed.push(flags))), ); return Command.runWith(command, { version: "0.0.0-test" })(args).pipe( Effect.provide(Layer.mergeAll(BunServices.layer, CliOutput.layer(silentFormatter))), - Effect.map(() => parsed[0]), ); }; +const parseWith = ( + config: Config, + args: ReadonlyArray, +) => { + const parsed: Array = []; + return runParse(config, args, parsed).pipe(Effect.map(() => parsed[0])); +}; + const rejectedMessage = ( config: Config, args: ReadonlyArray, ) => - parseWith(config, args).pipe( + runParse(config, args, []).pipe( Effect.flip, Effect.map((error) => { if (!CliError.isCliError(error) || error._tag !== "ShowHelp") { diff --git a/apps/cli/src/command-internal/target/unresolved-branch-suggestion.unit.test.ts b/apps/cli/src/command-internal/target/unresolved-branch-suggestion.unit.test.ts index 8a5a74bcbc..89f6e01f3b 100644 --- a/apps/cli/src/command-internal/target/unresolved-branch-suggestion.unit.test.ts +++ b/apps/cli/src/command-internal/target/unresolved-branch-suggestion.unit.test.ts @@ -1,5 +1,5 @@ import { Effect } from "effect"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "@effect/vitest"; import { LinkBranchListNetworkError, @@ -12,22 +12,26 @@ import { withUnresolvedBranchSuggestion } from "./unresolved-branch-suggestion.t const suggestion = "Try --from=local."; describe("withUnresolvedBranchSuggestion", () => { - it.each([ + it.effect.each([ new LinkBranchNotFoundError({ message: "not found" }), new LinkBranchNotLinkedError({ message: "not linked" }), new LinkParentRefInvalidError({ message: "bad link" }), - ])("adds the suggestion to $_tag and keeps its message", async (error) => { - const failure = await Effect.runPromise( - Effect.flip(Effect.fail(error).pipe(withUnresolvedBranchSuggestion(suggestion))), - ); - expect(failure).toMatchObject({ _tag: error._tag, message: error.message, suggestion }); - }); + ])("adds the suggestion to $_tag and keeps its message", (error) => + Effect.gen(function* () { + const failure = yield* Effect.flip( + Effect.fail(error).pipe(withUnresolvedBranchSuggestion(suggestion)), + ); + expect(failure).toMatchObject({ _tag: error._tag, message: error.message, suggestion }); + }), + ); - it("leaves other failures untouched", async () => { - const error = new LinkBranchListNetworkError({ message: "offline" }); - const failure = await Effect.runPromise( - Effect.flip(Effect.fail(error).pipe(withUnresolvedBranchSuggestion(suggestion))), - ); - expect(failure).toBe(error); - }); + it.effect("leaves other failures untouched", () => + Effect.gen(function* () { + const error = new LinkBranchListNetworkError({ message: "offline" }); + const failure = yield* Effect.flip( + Effect.fail(error).pipe(withUnresolvedBranchSuggestion(suggestion)), + ); + expect(failure).toBe(error); + }), + ); }); 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/bootstrap/bootstrap.integration.test.ts b/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts index 8cfb83c2a9..31c193bd24 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.integration.test.ts @@ -280,7 +280,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)), @@ -887,7 +887,7 @@ describeHostedConsent("bootstrap", { fakeDbConnection({}).dbConnectionLayer, login.layer, mockLoginCrypto().layer, - mockBrowser(), + mockBrowser, Layer.succeed(WorkdirFlag, Option.some(workdir)), Layer.succeed(DnsResolverFlag, "native"), Layer.succeed(NetworkIdFlag, Option.none()), 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 433d13d5e1..22f150a60e 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.layers.unit.test.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.layers.unit.test.ts @@ -95,7 +95,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 e3ae42b6d1..f2b513d8e6 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 @@ -241,7 +241,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/branches/branches.errors.ts b/apps/cli/src/commands/branches/branches.errors.ts index a149820667..3b50c50be4 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 660bb81c10..5443436b87 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 96b6e4ad73..363440864b 100644 --- a/apps/cli/src/commands/branches/create/create.integration.test.ts +++ b/apps/cli/src/commands/branches/create/create.integration.test.ts @@ -31,6 +31,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; @@ -462,6 +463,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", () => { 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 61e0f8d9ab..3f8afedf30 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, Layer, Option, FileSystem, Schema } from "effect"; +import { withJsonErrorHandling } from "../../../shared/output/json-error-handling.ts"; import { mockOutput, mockTty } from "../../../../tests/helpers/mocks.ts"; import { VALID_REF, @@ -169,6 +170,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 = buildRuntime({ 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 602a32106f..947600a118 100644 --- a/apps/cli/src/commands/branches/update/update.handler.ts +++ b/apps/cli/src/commands/branches/update/update.handler.ts @@ -92,6 +92,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/config/push/SIDE_EFFECTS.md b/apps/cli/src/commands/config/push/SIDE_EFFECTS.md index 7248efbd57..d5f686d9b4 100644 --- a/apps/cli/src/commands/config/push/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/config/push/SIDE_EFFECTS.md @@ -357,7 +357,7 @@ may itself contain a `.`. - **Resolution runs BEFORE the config load**, not after: a `[remotes.]` overlay is merged inside the resolved config load before its one full schema decode, and only one decode may ever run per invocation — reloading with a different `projectRef` a second time would wrongly reject a base document that's only valid once its matching remote's overlay applies. The accepted tradeoff: a branch name/UUID resolution's network call can fire even when the local `config.toml` turns out to be malformed (this only affects `--remote=`; a ref-shaped or absent target never needs a network call to resolve, so a malformed config there still aborts with zero requests made, matching this command's behavior before CLI-2168/CLI-2289). - 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 e4478a39bc..9f45c72670 100644 --- a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md @@ -34,7 +34,7 @@ it, and JSON `null` disables formatting without disabling safe compaction. | `/supabase/config.json` or `config.toml` | JSON / TOML | always, `config.json` preferred (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) | @@ -74,7 +74,7 @@ it, and JSON `null` disables formatting without disabling safe compaction. (`pgdelta-next-shadow.layer.ts`), which builds on the same shadow-baseline cache primitives (`acquireShadowDatabase`), with no declarative-schema-override branch. `--use-pgadmin` provisions its OWN shadow via a - narrower composition — `createShadowDatabase` -> health-wait -> `migrateShadowDatabase` + narrower composition — `createShadowDatabase` -> readiness wait (host Postgres connect probe) -> `migrateShadowDatabase` directly (`diff.handler.ts`'s pgadmin branch) — with no declarative-schema-override branch and no `targetUrlOverride`. - When `[experimental].stack` / `SUPABASE_EXPERIMENTAL_STACK=1` is on, the local target inspects the diff --git a/apps/cli/src/commands/db/diff/diff.handler.ts b/apps/cli/src/commands/db/diff/diff.handler.ts index d070a1bf4d..f5043d3e0f 100644 --- a/apps/cli/src/commands/db/diff/diff.handler.ts +++ b/apps/cli/src/commands/db/diff/diff.handler.ts @@ -58,7 +58,7 @@ import { stackWithShadowDatabase, } from "../../../command-internal/stack-shadow.ts"; import { isLocalDbRunning } from "../../../command-internal/db-bootstrap/local-db-running.ts"; -import { waitForHealthyServices } from "../../../command-internal/db-bootstrap/health-check.ts"; +import { waitForPostgresReady } from "../../../command-internal/db-bootstrap/health-check.ts"; import { withShadowDatabase } from "../../../command-internal/db-bootstrap/shadow-cache.ts"; import { createShadowDatabase, @@ -588,7 +588,7 @@ export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) { createShadowDatabase(spawner, shadowBase), (handle) => Effect.gen(function* () { - yield* waitForHealthyServices(spawner, [handle.containerId], { + yield* waitForPostgresReady(spawner, handle.containerId, shadowConnConfig, { timeoutSeconds: shadowBase.healthTimeoutSeconds, }); yield* migrateShadowDatabase(spawner, { diff --git a/apps/cli/src/commands/db/diff/diff.integration.test.ts b/apps/cli/src/commands/db/diff/diff.integration.test.ts index 07c3d0ae40..e634d2951a 100644 --- a/apps/cli/src/commands/db/diff/diff.integration.test.ts +++ b/apps/cli/src/commands/db/diff/diff.integration.test.ts @@ -19,7 +19,6 @@ import { } from "effect"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; -import { ChildProcess, ChildProcessSpawner } from "effect/process"; import type { DatabaseInstance, Stack } from "@supabase/stack/effect"; import { vi } from "vitest"; @@ -149,12 +148,10 @@ interface SetupOpts { // Fails the first `writeFileString` call whose path matches; prefer over // `failWriteOnCall` when shadow setup writes extra files first. readonly failWriteMatching?: (path: string) => boolean; - // Makes the shadow container never report healthy; only the `--use-pgadmin` branch - // gates on this (the shadow-source branch uses `neverConnectableShadow` instead). - readonly neverHealthyShadow?: boolean; - // Refuses every connect to the shadow's port, so `waitForShadowReady` keeps - // polling — the shadow-source branch's equivalent of `neverHealthyShadow`. + // Refuses every connect to the shadow's port, so `waitForPostgresReady` keeps polling. readonly neverConnectableShadow?: boolean; + // Docker reports every container's health check as unable to exec, as on a nested or sandboxed host. + readonly execFailedHealth?: boolean; // `CommandSettings.projectId`; defaults to `Option.some("test")`. Pass // `Option.none()` to exercise the config.toml/workdir-basename fallback. readonly projectId?: Option.Option; @@ -208,7 +205,7 @@ const SHADOW_PORT = 54320; /** * Records every `DbConnection.connect` target's database name and every `exec`/`query` run * against it. `neverConnectableShadow` fails every connect to the shadow port, leaving local - * connects untouched — this keeps a provisioning fiber suspended in `waitForShadowReady`'s + * connects untouched — this keeps a provisioning fiber suspended in `waitForPostgresReady`'s * retry loop. */ function fakeShadowDbConnection(opts: { readonly neverConnectableShadow?: boolean } = {}) { @@ -332,30 +329,10 @@ function setup(workdir: string, opts: SetupOpts = {}) { // A docker-spawner fake backs container lifecycle; a fake Postgres session backs // shadow setup. const shadowSpawner = mockShadowContainerCliSpawner({ - neverHealthy: opts.neverHealthyShadow ?? false, + execFailedHealth: opts.execFailedHealth ?? false, dbNotRunning: opts.dbNotRunning ?? false, dbInspectFailsWith: opts.dbInspectFailsWith, }); - const shadowHealthProbed = Deferred.makeUnsafe(); - const shadowSpawnerLayer = Layer.effect( - ChildProcessSpawner.ChildProcessSpawner, - Effect.map(ChildProcessSpawner.ChildProcessSpawner, (inner) => - ChildProcessSpawner.make((command) => - inner - .spawn(command) - .pipe( - Effect.tap(() => - ChildProcess.isStandardCommand(command) && - command.args[0] === "container" && - command.args[1] === "inspect" && - command.args[2] === FAKE_SHADOW_CONTAINER_ID - ? Deferred.succeed(shadowHealthProbed, undefined) - : Effect.void, - ), - ), - ), - ), - ).pipe(Layer.provide(shadowSpawner.layer)); // Cache tests need the stateful Docker model since `docker cp` needs real container state. const dockerDaemon = opts.statefulDocker === true ? mockDockerDaemonCliSpawner() : undefined; const shadowDbConnection = fakeShadowDbConnection({ @@ -597,7 +574,7 @@ function setup(workdir: string, opts: SetupOpts = {}) { edge, docker, shadowDbConnection.layer, - dockerDaemon?.layer ?? shadowSpawnerLayer, + dockerDaemon?.layer ?? shadowSpawner.layer, mockLocalDockerEngineUnavailableLayer, alwaysReadyHttpClientLayer, opts.realLayers === true @@ -657,7 +634,6 @@ function setup(workdir: string, opts: SetupOpts = {}) { differRegistryEnvAtCall, shadowSetupJobCalls, shadowSpawned: shadowSpawner.spawned, - shadowHealthProbed, dockerDaemon, shadowConnectedDatabases: shadowDbConnection.connectedDatabases, shadowExecCalls: shadowDbConnection.execCalls, @@ -772,6 +748,21 @@ describe("db diff", () => { }, ); + it.effect( + "diffs when Docker cannot exec the shadow's health check but the shadow accepts connections", + () => { + const s = setup(tmp.current, { + diffSql: "create table players ();\n", + execFailedHealth: true, + }); + return Effect.gen(function* () { + yield* dbDiff(flags()); + expect(stdout(s.out)).toBe("create table players ();\n\n"); + expect(s.shadowSpawned.filter((c) => c.args[0] === "rm")).toHaveLength(1); + }).pipe(Effect.provide(s.layer)); + }, + ); + it.effect("forwards SUPABASE_SSL_DEBUG=TRUE to the migra script as true", () => { const s = setup(tmp.current, { diffSql: "create table players ();\n" }); return Effect.gen(function* () { @@ -3039,7 +3030,7 @@ describe("db diff", () => { Effect.forkChild({ startImmediately: true }), ); // Waits until the shadow's readiness gate has refused a connect at least once, - // proving the fiber is suspended in `waitForShadowReady`'s retry loop. + // proving the fiber is suspended in `waitForPostgresReady`'s retry loop. yield* Deferred.await(s.shadowConnectRefused); // `Fiber.interrupt` only resolves once finalizers complete; this would hang (the readiness // timeout never elapses under the test clock) if `acquire` still covered the readiness wait. @@ -3085,6 +3076,21 @@ describe("db diff", () => { }, ); + it.effect( + "diffs when Docker cannot exec the shadow's health check but the shadow accepts connections", + () => { + const s = setup(tmp.current, { + pgadminStdout: [JSON.stringify([pgadminEntry()])], + execFailedHealth: true, + }); + return Effect.gen(function* () { + yield* dbDiff(flags({ usePgAdmin: Option.some(true) })); + expect(stdout(s.out)).toContain("ALTER TABLE test;"); + expect(s.shadowSpawned.filter((c) => c.args[0] === "rm")).toHaveLength(1); + }).pipe(Effect.provide(s.layer)); + }, + ); + it.effect("writes a timestamped migration for --use-pgadmin --file instead of printing", () => { const s = setup(tmp.current, { pgadminStdout: [JSON.stringify([pgadminEntry()])] }); return Effect.gen(function* () { @@ -3543,16 +3549,13 @@ describe("db diff", () => { it.effect( "removes the shadow container on interruption during the health wait for --use-pgadmin too", () => { - const s = setup(tmp.current, { usePgDelta: true, neverHealthyShadow: true }); + const s = setup(tmp.current, { usePgDelta: true, neverConnectableShadow: true }); return Effect.gen(function* () { const fiber = yield* dbDiff(flags({ usePgAdmin: Option.some(true) })).pipe( Effect.provide(s.layer), Effect.forkChild({ startImmediately: true }), ); - // Waits for the shadow's own health probe (its 64-hex id); the pgadmin path's - // separate `supabase_db_test` probe fires first and would satisfy a looser - // check immediately. - yield* Deferred.await(s.shadowHealthProbed); + yield* Deferred.await(s.shadowConnectRefused); yield* Fiber.interrupt(fiber); expect(s.shadowSpawned.filter((c) => c.args[0] === "create")).toHaveLength(1); expect(s.shadowSpawned.filter((c) => c.args[0] === "rm")).toHaveLength(1); diff --git a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md index 064b3a9cac..8fd54cf5cd 100644 --- a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md @@ -39,18 +39,18 @@ image), to stdout or `--file`. ## Environment Variables -| Variable | Purpose | -| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `SUPABASE_DB_PASSWORD` (`--password`/`-p` overrides) | remote DB password; ignored for a target other than the linked project (see Notes) | -| `SUPABASE_ACCESS_TOKEN` | auth for a remote project or branch | -| `SUPABASE_PROJECT_ID` | linked project ref override and the parent that `--remote=` is resolved against (superseded by `--remote=`) | -| `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 | remote 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` (`--password`/`-p` overrides) | remote DB password; ignored for a target other than the linked project (see Notes) | +| `SUPABASE_ACCESS_TOKEN` | auth for a remote project or branch | +| `SUPABASE_PROJECT_ID` | linked project ref override and the parent that `--remote=` is resolved against (superseded by `--remote=`) | +| `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 | remote 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 3e18f50e00..8078340877 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -132,20 +132,20 @@ at all, so nothing is cached for it. ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth for a remote project or branch | no | -| `SUPABASE_DB_PASSWORD` | remote DB password (overridden by `-p`); ignored for a target other than the linked project (see Notes) | 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_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback and the parent project `--remote=` resolves against; `--remote=` supersedes it for the ref — 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_PGDELTA_ENABLED` | overrides `[experimental.pgdelta].enabled` (default true; `false` selects migra for the migration-style diff unless `--diff-engine` is passed, and lets a declarative export write `schema_paths`); a value that is not a boolean fails the command | no | -| `SUPABASE_EXPERIMENTAL` | resolving true (shell, then project `.env`) without `--declarative` fails the command with the removed structured-dump error; `--experimental=false` overrides it for one invocation | no | -| `PG*` libpq | remote-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 a remote project or branch | no | +| `SUPABASE_DB_PASSWORD` | remote DB password (overridden by `-p`); ignored for a target other than the linked project (see Notes) | 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_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback and the parent project `--remote=` resolves against; `--remote=` supersedes it for the ref — 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_PGDELTA_ENABLED` | overrides `[experimental.pgdelta].enabled` (default true; `false` selects migra for the migration-style diff unless `--diff-engine` is passed, and lets a declarative export write `schema_paths`); a value that is not a boolean fails the command | no | +| `SUPABASE_EXPERIMENTAL` | resolving true (shell, then project `.env`) without `--declarative` fails the command with the removed structured-dump error; `--experimental=false` overrides it for one invocation | no | +| `PG*` libpq | remote-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 d1576b5d11..77a70121ff 100644 --- a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md @@ -106,6 +106,16 @@ equivalent, PG15) or `InitSchema14`/`ApplyApiPrivileges` (PG14). | `docker container inspect ` + `docker exec kong reload --nginx-conf /home/kong/custom_nginx.template` | local path, both PG14 and PG15 | reload Kong so it re-resolves the restarted containers' addresses (issue #6016) — the `--nginx-conf` flag is load-bearing: a bare `kong reload` regenerates nginx.conf from Kong's default template and drops the custom `email_templates` server (#6059) | | `docker container inspect supabase_storage_` | local path | storage-health gate before bucket seeding | +The recreated database's readiness (PG15 after `docker start`, PG14 after `docker restart`) is not +read from Docker's health status, which needs `docker exec` that some hosts cannot do. It polls +every 500ms within `db.health_timeout`: `docker container inspect supabase_db_` (a +container that is not running fails at once), then a short-lived host connect as `postgres` to the +published Postgres address (2s dial cap). Before the command returns, it waits up to 15s for +Docker's first health verdict on the database container (polling `docker container inspect` every +1s until the status leaves `starting`, the container stops running, or Docker cannot launch its +health check), so a following `supabase status` does not see `starting`. That wait never fails the +reset. + No subprocess delegation on either target — the remote path's `--experimental` schema-files apply is fully native. @@ -164,25 +174,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 a remote project or branch | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_DB_PASSWORD` | password for the remote connection; ignored for a target other than the linked project (see Notes) | no | -| `SUPABASE_DB_SEED_ENABLED` | overrides `[db.seed].enabled` (flag > env > matched `[remotes.*]` > base); honoured on the local and remote paths | no | -| `SUPABASE_YES` | grants the hosted reset consent (shell only; a `SUPABASE_YES` in `supabase/.env` does not count) and auto-confirms 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` (default true); `false` opens the reset gate (`experimental && resolvedVersion === "" && !toml.pgDelta.enabled`) so `--experimental` applies `schema_paths` files instead of timestamped migrations — switches between two different destructive code paths | no | -| `SUPABASE_DB_MIGRATIONS_SCHEMA_PATHS` | overrides `[db.migrations].schema_paths` (beats the config-file value and a matched `[remotes.*]` block) 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 and the parent project `--remote=` resolves against; `--remote=` supersedes it for the ref — 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) | -| `PG*` libpq | remote 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 a remote project or branch | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_DB_PASSWORD` | password for the remote connection; ignored for a target other than the linked project (see Notes) | no | +| `SUPABASE_DB_SEED_ENABLED` | overrides `[db.seed].enabled` (flag > env > matched `[remotes.*]` > base); honoured on the local and remote paths | no | +| `SUPABASE_YES` | grants the hosted reset consent (shell only; a `SUPABASE_YES` in `supabase/.env` does not count) and auto-confirms 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` (default true); `false` opens the reset gate (`experimental && resolvedVersion === "" && !toml.pgDelta.enabled`) so `--experimental` applies `schema_paths` files instead of timestamped migrations — switches between two different destructive code paths | no | +| `SUPABASE_DB_MIGRATIONS_SCHEMA_PATHS` | overrides `[db.migrations].schema_paths` (beats the config-file value and a matched `[remotes.*]` block) 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 and the parent project `--remote=` resolves against; `--remote=` supersedes it for the ref — 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) | +| `PG*` libpq | remote TLS, timeout, service; see `db push` | no | ## Connection loss during migration apply 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 e9c1aede15..780c74e541 100644 --- a/apps/cli/src/commands/db/reset/reset.integration.test.ts +++ b/apps/cli/src/commands/db/reset/reset.integration.test.ts @@ -55,6 +55,7 @@ import { describeDbTargetSelection, } from "../../../../tests/helpers/db-target-selection.ts"; import { branchListApi } from "../../../../tests/helpers/remote-selection.ts"; +import { EXEC_FAILED_STATE } from "../../../../tests/helpers/docker-health.ts"; import { assertPermissionDrift } from "../../../../tests/helpers/permission-drift.ts"; import { asRecordedRequests, @@ -121,7 +122,7 @@ import { TargetPolicy } from "../../../command-internal/target/target-policy.ts" import { DbConfigConnectTempRoleError } from "../../../command-internal/db-config.errors.ts"; import { generateLocalJwt } from "../../../command-internal/local-jwt.ts"; import { LocalDockerEngine } from "../../../command-internal/db-bootstrap/local-db-running.ts"; -import { DbExecError } from "../../../command-internal/db-connection.errors.ts"; +import { DbConnectError, DbExecError } from "../../../command-internal/db-connection.errors.ts"; import { DbConnection, type PgConnInput, @@ -224,6 +225,8 @@ function mockConnection( /** When set, an `exec` whose SQL contains this substring fails instead of succeeding. */ execFailsOn?: string; execFailsMessage?: string; + /** Refuses every connect, so the local recreate's Postgres readiness probe never succeeds. */ + neverConnectable?: boolean; } = {}, ) { const execs: Array = []; @@ -231,6 +234,9 @@ function mockConnection( let replicationCallIndex = 0; const layer = Layer.succeed(DbConnection, { connect: () => { + if (opts.neverConnectable === true) { + return Effect.fail(new DbConnectError({ message: "connection refused" })); + } const session: DbSession = { extensionExists: () => Effect.succeed(false), copyToCsv: () => Effect.succeed(new Uint8Array()), @@ -300,7 +306,6 @@ const KONG_ID = `supabase_kong_${PROJECT_ID}`; const STORAGE_ID = `supabase_storage_${PROJECT_ID}`; const HEALTHY_STATE = '{"Running":true,"Status":"running","Health":{"Status":"healthy"}}'; -const STARTING_STATE = '{"Running":true,"Status":"running","Health":{"Status":"starting"}}'; const STOPPED_STATE = '{"Running":false,"Status":"exited"}'; interface SpawnRecord { @@ -398,7 +403,6 @@ const dbSetupJobCalls = (spawned: ReadonlyArray): ReadonlyArray>; execFailsOn?: string; execFailsMessage?: string; + neverConnectable?: boolean; yes?: boolean; interactive?: boolean; noSeed?: boolean; @@ -1198,6 +1202,28 @@ describe("db reset", () => { ); }); + it.effect( + "recreates when Docker cannot exec the database's health check but it accepts connections", + () => { + const base = defaultLocalResetRoute(); + const { layer, out, child } = setup(tmp.current, { + toml: 'project_id = "test"\n', + args: ["db", "reset", "--local"], + isLocal: true, + route: (args) => + args[0] === "container" && args[1] === "inspect" && args[2] === DB_ID + ? { stdout: [EXEC_FAILED_STATE] } + : base(args), + }); + return Effect.gen(function* () { + yield* dbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + + expect(dbSetupJobCalls(child.spawned)).toHaveLength(3); + expect(out.stderrText).toContain("Finished supabase db reset.\n"); + }); + }, + ); + it.effect( "passes the resolved --version through to the setup pipeline's seed/migrate step", () => { @@ -1398,7 +1424,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"], @@ -1406,6 +1431,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("supabase.lock"))).toBe(true); expect(legacyStateAccesses(accessed)).toEqual([]); @@ -2279,6 +2305,28 @@ describe("db reset", () => { }); describe("local reset — PG14", () => { + it.effect( + "resets when Docker cannot exec the restarted database's health check but it accepts connections", + () => { + const base = defaultLocalResetRoute(); + const { layer, out, child } = setup(tmp.current, { + toml: PG14_TOML, + args: ["db", "reset", "--local"], + isLocal: true, + route: (args) => + args[0] === "container" && args[1] === "inspect" && args[2] === DB_ID + ? { stdout: [EXEC_FAILED_STATE] } + : base(args), + }); + return Effect.gen(function* () { + yield* dbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + + expect(restartedContainers(child.spawned)).toContain(DB_ID); + expect(out.stderrText).toContain("Finished supabase db reset.\n"); + }); + }, + ); + it.effect( "recreates via the four-statement DROP/CREATE sequence, then initDatabase + RestartDatabase", () => { @@ -2633,12 +2681,12 @@ describe("db reset", () => { }); describe("local reset — health timeouts", () => { - it.live("a container health-check timeout fails the whole recreate", () => { + it.live("a database that never accepts connections fails the whole recreate", () => { const { layer } = setup(tmp.current, { toml: `project_id = "test"\n${FAST_HEALTH_TOML}`, args: ["db", "reset"], isLocal: true, - routeOpts: { neverHealthy: true }, + neverConnectable: true, }); return Effect.gen(function* () { const exit = yield* dbReset(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); 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 c90fc44a3d..169174d817 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 @@ -46,17 +46,17 @@ formatting without disabling safe compaction. ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for a remote project or branch | no | -| `SUPABASE_DB_PASSWORD` | remote database password for a remote project or branch (`--password`/`-p` overrides) | -| `SUPABASE_PROJECT_ID` | the project ref of a bare `--remote` target, and the parent that `--remote=` is resolved against | -| `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 a local target | 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 | -| `PG*` libpq | remote-project TLS, timeout, service; see `db push` | no | +| Variable | Purpose | Required? | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for a remote project or branch | no | +| `SUPABASE_DB_PASSWORD` | remote database password for a remote project or branch (`--password`/`-p` overrides) | +| `SUPABASE_PROJECT_ID` | the project ref of a bare `--remote` target, and the parent that `--remote=` is resolved against | +| `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 a local target | 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 | +| `PG*` libpq | remote-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 4e77cc31fa..8228f1c405 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**: remote-project TLS, timeout, service; see `db push`. Sync itself has no target 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**: remote-project TLS, timeout, service; see `db push`. Sync itself has no target flag | no | ## Exit Codes 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 6f98f7b5e5..7a53c56757 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 @@ -1003,21 +1003,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/db/shared/pgdelta-next-shadow.layer.ts b/apps/cli/src/commands/db/shared/pgdelta-next-shadow.layer.ts index 252850263c..1636c9384b 100644 --- a/apps/cli/src/commands/db/shared/pgdelta-next-shadow.layer.ts +++ b/apps/cli/src/commands/db/shared/pgdelta-next-shadow.layer.ts @@ -18,7 +18,7 @@ import { buildLocalDbContainerInputs, type LocalDbContainerInputs, } from "../../../command-internal/db-bootstrap/local-container-inputs.ts"; -import { waitForShadowReady } from "../../../command-internal/db-bootstrap/health-check.ts"; +import { waitForPostgresReady } from "../../../command-internal/db-bootstrap/health-check.ts"; import { acquireShadowDatabase, peekShadowBaseline, @@ -251,7 +251,7 @@ export const pgDeltaNextShadowLayer = Layer.effect( ); const awaitShadowReady = (input: NativeShadowInput, handle: ShadowAcquiredHandle) => - waitForShadowReady(input.spawner, handle.containerId, shadowConnConfig(input.base), { + waitForPostgresReady(input.spawner, handle.containerId, shadowConnConfig(input.base), { timeoutSeconds: input.base.healthTimeoutSeconds, image: input.base.image, }); 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 b65fe6ddf0..e7d675137b 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 @@ -53,28 +53,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 @@ -295,6 +298,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 ec3862493f..499fa47384 100644 --- a/apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts +++ b/apps/cli/src/commands/db/shared/pgdelta.seam.layer.ts @@ -257,7 +257,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/shared/shadow-source.ts b/apps/cli/src/commands/db/shared/shadow-source.ts index 655563b29c..3134f68ed4 100644 --- a/apps/cli/src/commands/db/shared/shadow-source.ts +++ b/apps/cli/src/commands/db/shared/shadow-source.ts @@ -28,7 +28,7 @@ import { import type { DockerRun } from "../../../command-internal/docker-run.service.ts"; import type { ImagePrepullError } from "../../../command-internal/db-bootstrap/image-prepull.ts"; import type { HealthCheckTimeoutError } from "../../../command-internal/db-bootstrap/health-check.ts"; -import { waitForShadowReady } from "../../../command-internal/db-bootstrap/health-check.ts"; +import { waitForPostgresReady } from "../../../command-internal/db-bootstrap/health-check.ts"; import { seedGlobals } from "../../../command-internal/migration-apply.ts"; import { BAD_PATTERN_MESSAGE, pathMatch } from "../../../command-internal/path-match.ts"; import { toPostgresURL } from "../../../command-internal/postgres-url.ts"; @@ -95,7 +95,7 @@ export const prepareShadowSource = Effect.fn("ShadowSource.prepare")(function* < database: "postgres", }; - yield* waitForShadowReady(spawner, containerId, connConfig, { + yield* waitForPostgresReady(spawner, containerId, connConfig, { timeoutSeconds: input.healthTimeoutSeconds, image: input.image, }); diff --git a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md index 27a454cd20..cd4c6dfff3 100644 --- a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md @@ -39,10 +39,16 @@ 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 - restore can exceed the timeout) — the container-logs dump to stderr still happens +6. Wait for Postgres to accept connections (`db.health_timeout`, default `2m`): every 500ms, + `docker container inspect` (a container that is not running fails the wait at once), then a + short-lived host connect to the published Postgres address (2s dial cap) — Docker's health + status is not read, because it needs `docker exec`, which nested or sandboxed Docker cannot + do, and lags connectability by up to one 10s interval. A timeout fails the command with + ` container is not ready: ` UNLESS `--from-backup` is set, in + which case it is swallowed (a large restore can exceed the timeout) — the container-logs dump + to stderr, and the `exec format error` recovery advice when the logs show it, still happen either way. 7. On a fresh volume with `--from-backup` unset: run the `SetupLocalDatabase`-equivalent pipeline (`command-internal/db-bootstrap/db-setup.ts`) — initial schema (PG<=14: SQL over a @@ -60,6 +66,10 @@ composition reuses too — see that command's `SIDE_EFFECTS.md`): that reaches this point (fresh volume, existing volume, and a swallowed `--from-backup` health-check timeout), but NOT on the already-running short-circuit or the `backup volume already exists` guard. +9. Wait up to 15s for Docker's first health verdict, so `supabase db start && supabase status` + does not see `starting`: poll `docker container inspect` every 1s until the health status + leaves `starting`, the container stops running, or Docker cannot launch its health check. Never + fails the command, whatever the outcome, and is not covered by the rollback below. Any failure from step 1 onward rolls back via `rollbackStart` (stop + prune every container/network this project's label matches; volumes are pruned too, but ONLY when the @@ -107,7 +117,7 @@ only spawns on fallback. | `docker image inspect` / `docker pull` (registry-fallback resolve) | when not already running — resolves the Postgres image | | `docker volume create --label ...` | when not already running, unless a `--from-backup` restore onto an existing volume (fails first) | | `docker create` + optional `docker cp - :/` + `docker start` | when not already running — `cp` streams one in-memory tar archive containing the pgsodium root key at its exact path when the selected entrypoint needs it | -| `docker container inspect` (repeated) | health-wait polling, 1s constant backoff up to `db.health_timeout` | +| `docker container inspect` (repeated) | readiness polling every 500ms up to `db.health_timeout`, then health-settle polling every 1s for up to 15s | | `docker logs ` | on a health-check timeout (either path — swallowed or not) | | `docker run --rm ...` | fresh volume, no `--from-backup`, `db.major_version >= 15`: up to 3 one-shot migrate jobs (realtime/storage/auth) | | `docker ps` / `docker stop` / `docker container prune` / `docker volume prune` (fresh-volume runs only) / `docker network prune` | on ANY failure from network-ensure through `_current_branch` — the rollback | @@ -216,8 +226,8 @@ The resident owner process manages the database beyond the CLI invocation. Stack with webhooks disabled DROP a `pg_net` that migration history does not own. `pg_net` installed outside migrations (Studio SQL editor) is dropped on the next start — accepted, documented edge. -- `--from-backup` restores the database from a logical backup file on start; the health - check is skipped (not failed) for backups — a large restore can exceed +- `--from-backup` restores the database from a logical backup file on start; a readiness-wait + timeout is swallowed (not failed) for backups — a large restore can exceed `db.health_timeout`. - No `cli_stack_started` telemetry — that event belongs to `supabase start`, not `db start`. The only event is the standard `cli_command_executed`. diff --git a/apps/cli/src/commands/db/start/start.integration.test.ts b/apps/cli/src/commands/db/start/start.integration.test.ts index 590a66dea5..b31ddf7adc 100644 --- a/apps/cli/src/commands/db/start/start.integration.test.ts +++ b/apps/cli/src/commands/db/start/start.integration.test.ts @@ -2,8 +2,10 @@ import { BunServices } from "@effect/platform-bun"; import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest"; import { Cause, + Deferred, Effect, Exit, + Fiber, FileSystem, Layer, Option, @@ -16,6 +18,7 @@ import { import { ChildProcess, ChildProcessSpawner } from "effect/process"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; +import * as TestClock from "effect/testing/TestClock"; import { vi } from "vitest"; import { @@ -23,6 +26,7 @@ import { mockProcessControl, mockRuntimeInfo, } from "../../../../tests/helpers/mocks.ts"; +import { EXEC_FAILED_STATE } from "../../../../tests/helpers/docker-health.ts"; import { assertPermissionDrift } from "../../../../tests/helpers/permission-drift.ts"; import { configValuesLayer } from "../../../../tests/helpers/config-values-layer.ts"; import { unusedGateway } from "../../../../tests/helpers/unused-stack.ts"; @@ -177,7 +181,7 @@ function volumePruneWasAttempted(spawned: ReadonlyArray): boolean { } /** Stateful default route: only created containers inspect successfully, matching Docker across initial state detection and post-create health waits. Existing volume by default (a restart). */ -function defaultRoute(opts: { readonly neverHealthy?: boolean } = {}) { +function defaultRoute() { const created = new Set(); return (args: ReadonlyArray): RouteResult => { if (args[0] === "image" && args[1] === "inspect") return { exitCode: 0 }; @@ -197,7 +201,6 @@ function defaultRoute(opts: { readonly neverHealthy?: boolean } = {}) { if (!created.has(id)) { return { exitCode: 1, stderr: [`Error: No such container: ${id}`] }; } - if (opts.neverHealthy === true) return { stdout: [STARTING_STATE] }; return { stdout: [HEALTHY_STATE] }; } if (args[0] === "logs") return { exitCode: 0 }; @@ -218,6 +221,18 @@ function freshVolumeRoute( }; } +/** Docker's HEALTHCHECK can never launch on this host, so every created container reports exec-failed health. */ +function execFailedHealthRoute( + base: (args: ReadonlyArray) => RouteResult, +): (args: ReadonlyArray) => RouteResult { + return (args) => { + const result = base(args); + return args[0] === "container" && args[1] === "inspect" && result.stdout?.[0] === HEALTHY_STATE + ? { stdout: [EXEC_FAILED_STATE] } + : result; + }; +} + /** * Makes `isLocalDbRunning`'s pre-bring-up `container inspect` succeed unconditionally, * simulating an already-up local db — the already-running short-circuit returns before any @@ -312,8 +327,10 @@ interface SetupOpts { readonly experimental?: boolean; /** `--debug`. Defaults to `false`. */ readonly debug?: boolean; - /** Number of initial `DbConnection.connect` attempts that fail before succeeding. */ + /** Number of consecutive `DbConnection.connect` attempts that fail, from `connectFailuresStartAt`. */ readonly connectFailures?: number; + /** 1-based attempt number of the first failing connect. Attempt 1 is the Postgres readiness probe. Defaults to 1. */ + readonly connectFailuresStartAt?: number; /** Whether the mocked connect failures are dial-level (`retryable`). Defaults to `true`. */ readonly connectFailuresRetryable?: boolean; /** Record catalog apply targets instead of the default noop. */ @@ -354,11 +371,15 @@ function setup(opts: SetupOpts = {}) { let connectAttempts = 0; const connectFailures = opts.connectFailures ?? 0; + const connectFailuresStartAt = opts.connectFailuresStartAt ?? 1; const dbConnection = Layer.succeed(DbConnection, { connect: () => Effect.suspend(() => { connectAttempts += 1; - if (connectAttempts <= connectFailures) { + if ( + connectAttempts >= connectFailuresStartAt && + connectAttempts < connectFailuresStartAt + connectFailures + ) { return Effect.fail( new DbConnectError({ message: @@ -502,10 +523,14 @@ describe("db start", () => { it.live( "fresh volume: retries the host connect while the published port is not yet reachable (#6136)", () => { - const s = setup({ route: freshVolumeRoute(defaultRoute()), connectFailures: 2 }); + const s = setup({ + route: freshVolumeRoute(defaultRoute()), + connectFailuresStartAt: 2, + connectFailures: 2, + }); return Effect.gen(function* () { yield* dbStart(DEFAULT_FLAGS).pipe(Effect.provide(s.layer)); - expect(s.connectAttempts).toBe(3); + expect(s.connectAttempts).toBe(4); expect(yield* readCurrentBranch(tempRoot.current)).toBe("main"); }); }, @@ -515,13 +540,14 @@ describe("db start", () => { it.effect("fresh volume: a non-dial connect failure is not retried", () => { const s = setup({ route: freshVolumeRoute(defaultRoute()), + connectFailuresStartAt: 2, connectFailures: 1, connectFailuresRetryable: false, }); return Effect.gen(function* () { const exit = yield* dbStart(DEFAULT_FLAGS).pipe(Effect.provide(s.layer), Effect.exit); expect(Exit.isFailure(exit)).toBe(true); - expect(s.connectAttempts).toBe(1); + expect(s.connectAttempts).toBe(2); }); }); @@ -713,13 +739,63 @@ describe("db start", () => { }); }); + it.effect("starts when Docker's health check cannot exec but the host can connect", () => { + const s = setup({ route: execFailedHealthRoute(freshVolumeRoute(defaultRoute())) }); + return Effect.gen(function* () { + yield* dbStart(DEFAULT_FLAGS).pipe(Effect.provide(s.layer)); + expect(s.connectAttempts).toBeGreaterThan(0); + expect(rollbackWasAttempted(s.child.spawned)).toBe(false); + expect(yield* readCurrentBranch(tempRoot.current)).toBe("main"); + }); + }); + + it.effect("waits for Docker's first health verdict once the database accepts connections", () => { + const settleStarted = Deferred.makeUnsafe(); + const settleDone = Deferred.makeUnsafe(); + const base = freshVolumeRoute(defaultRoute()); + let inspectsAfterConnect = 0; + const s = setup({ + route: (args) => { + const result = base(args); + const isDbInspect = + args[0] === "container" && + args[1] === "inspect" && + result.stdout?.[0] === HEALTHY_STATE && + s.connectAttempts > 0; + if (!isDbInspect) return result; + inspectsAfterConnect += 1; + Deferred.doneUnsafe(settleStarted, Effect.void); + if (inspectsAfterConnect < 3) return { stdout: [STARTING_STATE] }; + Deferred.doneUnsafe(settleDone, Effect.void); + return result; + }, + }); + return Effect.gen(function* () { + const fiber = yield* dbStart(DEFAULT_FLAGS).pipe( + Effect.provide(s.layer), + Effect.forkChild({ startImmediately: true }), + ); + yield* Effect.raceFirst(Deferred.await(settleStarted), Fiber.await(fiber)); + // Small steps: the settle's own 15s cap must stay far away however many scheduler turns + // each poll needs before it registers its next sleep. + while (fiber.pollUnsafe() === undefined && !(yield* Deferred.isDone(settleDone))) { + yield* TestClock.adjust("100 millis"); + } + const exit = yield* Fiber.await(fiber); + + expect(Exit.isSuccess(exit)).toBe(true); + expect(inspectsAfterConnect).toBe(3); + }); + }); + it.live("a health-check timeout without --from-backup fails the command and rolls back", () => { // `db.health_timeout` (unlike the generic 30s `serviceTimeout` every other service waits on) // is a real config.toml-configurable seam — this keeps the scenario fast instead of waiting // out the real 2m default. const { layer, child } = setup({ configContents: 'project_id = "test"\n[db]\nhealth_timeout = "1s"\n', - route: freshVolumeRoute(defaultRoute({ neverHealthy: true })), + route: freshVolumeRoute(defaultRoute()), + connectFailures: Number.POSITIVE_INFINITY, }); return Effect.gen(function* () { const exit = yield* dbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer), Effect.exit); @@ -739,7 +815,8 @@ describe("db start", () => { // it first. const { layer, child } = setup({ configContents: 'project_id = "test"\n[db]\nhealth_timeout = "1s"\n', - route: freshVolumeRoute(defaultRoute({ neverHealthy: true })), + route: freshVolumeRoute(defaultRoute()), + connectFailures: Number.POSITIVE_INFINITY, projectEnvContents: "SUPABASE_DEBUG=true\n", }); // This log write goes straight to the real process stderr, never the mocked Output @@ -771,7 +848,8 @@ describe("db start", () => { () => { const { layer, child } = setup({ configContents: 'project_id = "test"\n[db]\nhealth_timeout = "1s"\n', - route: freshVolumeRoute(defaultRoute({ neverHealthy: true })), + route: freshVolumeRoute(defaultRoute()), + connectFailures: Number.POSITIVE_INFINITY, }); return Effect.gen(function* () { // The stderr log dump on timeout is covered by the shared health-check.ts suite; this 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 6b76d59202..bd63302008 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 @@ -1004,7 +1004,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/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md index b045a7f727..4ee0ca965b 100644 --- a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md @@ -55,18 +55,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 and the parent project that `--remote=` is resolved against; 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 and the parent project that `--remote=` is resolved against; 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/deploy/deploy.integration.test.ts b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts index 634a43c787..8bcbff1ee3 100644 --- a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts +++ b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts @@ -2667,7 +2667,7 @@ describe("functions deploy", () => { ), ), ); - seedLegacyStateDecoys(tempRoot.current); + yield* seedLegacyStateDecoys(tempRoot.current); yield* functionsDeploy({ ...baseFlags, useApi: false, useDocker: true }); @@ -2727,7 +2727,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/download/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md index afc34c2436..a440f5df53 100644 --- a/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/download/SIDE_EFFECTS.md @@ -63,7 +63,7 @@ in text mode, 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/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/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md index 39a636a50f..7ca04c2328 100644 --- a/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/serve/SIDE_EFFECTS.md @@ -47,18 +47,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` | overrides `project_id` for resource names | no | -| `SUPABASE_ENV` | selects environment-specific dotenv files (`.env..local`, `.env.`) | no (defaults to `development`) | -| env vars referenced by `supabase/config.toml` | config interpolation; shell values win over the project `.env*` files | 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` | overrides `project_id` for resource names | no | +| `SUPABASE_ENV` | selects environment-specific dotenv files (`.env..local`, `.env.`) | no (defaults to `development`) | +| env vars referenced by `supabase/config.toml` | config interpolation; shell values win over the project `.env*` files | 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/functions/serve/serve.stack.e2e.test.ts b/apps/cli/src/commands/functions/serve/serve.stack.e2e.test.ts index f5782abc54..1bc97ba32a 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 { generateLocalJwt } from "../../../command-internal/local-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 f01b42c12f..791305d159 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 @@ -445,7 +445,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 2011e36035..1a52bfaa32 100644 --- a/apps/cli/src/commands/gen/types/types.integration.test.ts +++ b/apps/cli/src/commands/gen/types/types.integration.test.ts @@ -3290,7 +3290,7 @@ describe("gen types", () => { }), ), }); - seedLegacyStateDecoys(workdir); + yield* seedLegacyStateDecoys(workdir); yield* genTypes(defaultFlags({ local: true })).pipe( Effect.provide( 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 6d07780e78..5e1d2052c5 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 @@ -179,15 +179,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/squash/SIDE_EFFECTS.md b/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md index 5af32e762e..c03f8ce3ff 100644 --- a/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/migration/squash/SIDE_EFFECTS.md @@ -25,7 +25,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 | @@ -61,7 +61,7 @@ live shadow available. - Network ensure (`ensureNetwork`, same as `db diff`/`db pull`). - Shadow Postgres container: no `--name`, no network alias, `--publish :5432`, `-c max_worker_processes=0`, `--rm`, PG≤14 tmpfs on `/docker-entrypoint-initdb.d` — created, - started, health-polled (`container inspect`), then removed (`rm -f -v`) once squash finishes, + started, readiness-polled (`container inspect` plus a host Postgres connect probe), then removed (`rm -f -v`) once squash finishes, success or failure. - PG15+ one-shot realtime/storage/auth migrate jobs (`initSchema15`), dialed at the shadow container's own 12-char short id as `DB_HOST` (no name/alias needed — see @@ -94,7 +94,7 @@ live shadow available. `SUPABASE_YES` (shell only; confirms a hosted run), `SUPABASE_PROJECT_ID`, `SUPABASE_DB_PASSWORD` (remote project only), `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 a remote project (TLS, timeout, service; see `db push`). ## Exit Codes diff --git a/apps/cli/src/commands/migration/squash/squash.handler.ts b/apps/cli/src/commands/migration/squash/squash.handler.ts index f1b501ef47..f9c5223fc1 100644 --- a/apps/cli/src/commands/migration/squash/squash.handler.ts +++ b/apps/cli/src/commands/migration/squash/squash.handler.ts @@ -19,7 +19,7 @@ import { resolveDbSetupPrelude, setupDatabase, } from "../../../command-internal/db-bootstrap/db-setup.ts"; -import { waitForHealthyServices } from "../../../command-internal/db-bootstrap/health-check.ts"; +import { waitForPostgresReady } from "../../../command-internal/db-bootstrap/health-check.ts"; import { buildShadowSetupDatabaseInput, connectShadowDatabase, @@ -199,7 +199,7 @@ const squashMigrations = Effect.fn("MigrationSquash.squashMigrations")(function* (handle) => Effect.scoped( Effect.gen(function* () { - yield* waitForHealthyServices(spawner, [handle.containerId], { + yield* waitForPostgresReady(spawner, handle.containerId, connConfig, { timeoutSeconds: shadowInput.healthTimeoutSeconds, }); const session = yield* connectShadowDatabase(connConfig); diff --git a/apps/cli/src/commands/migration/squash/squash.integration.test.ts b/apps/cli/src/commands/migration/squash/squash.integration.test.ts index ba6cecc8fa..a5a5707bb6 100644 --- a/apps/cli/src/commands/migration/squash/squash.integration.test.ts +++ b/apps/cli/src/commands/migration/squash/squash.integration.test.ts @@ -69,7 +69,7 @@ import type { DbConnectionInput, ResolvedDbConnection, } from "../../../command-internal/db-config.types.ts"; -import { DbExecError } from "../../../command-internal/db-connection.errors.ts"; +import { DbConnectError, DbExecError } from "../../../command-internal/db-connection.errors.ts"; import { DbConnection, type DbSession, @@ -245,7 +245,9 @@ interface SetupOpts { readonly failResolve?: boolean; readonly failSql?: string; readonly networkId?: string; - readonly neverHealthyShadow?: boolean; + readonly neverConnectableShadow?: boolean; + /** Docker reports every container's health check as unable to exec, as on a nested or sandboxed host. */ + readonly execFailedHealth?: boolean; readonly failCreateShadow?: boolean; readonly failRemoveShadow?: boolean; readonly failSetupJob?: boolean; @@ -259,7 +261,7 @@ interface SetupOpts { function fakeServices(workdir: string, opts: SetupOpts = {}) { const spawner = mockShadowContainerCliSpawner({ - neverHealthy: opts.neverHealthyShadow ?? false, + execFailedHealth: opts.execFailedHealth ?? false, failCreate: opts.failCreateShadow ?? false, failRemove: opts.failRemoveShadow ?? false, }); @@ -280,8 +282,11 @@ function fakeServices(workdir: string, opts: SetupOpts = {}) { const connectedDatabases: Array = []; const connection = Layer.succeed(DbConnection, { connect: (cfg: PgConnInput) => - Effect.sync(() => { + Effect.gen(function* () { connectedDatabases.push(cfg.database); + if (opts.neverConnectableShadow === true) { + return yield* new DbConnectError({ message: "connection refused" }); + } const session: DbSession = { exec: (sql: string) => Effect.suspend(() => { @@ -687,6 +692,21 @@ describe("migration squash", () => { }, ); + it.effect( + "squashes when Docker cannot exec the shadow's health check but the shadow accepts connections", + () => { + const s = setupHappyPath({ execFailedHealth: true }); + return Effect.gen(function* () { + yield* seedHappyPathMigrations(tmp.current); + yield* migrationSquash(flags()); + + expect(yield* migrationExists(tmp.current, "0_init.sql")).toBe(false); + expect(yield* migrationExists(tmp.current, "1_target.sql")).toBe(true); + expect(s.shadowSpawned.filter((c) => c.args[0] === "rm")).toHaveLength(1); + }).pipe(Effect.provide(s.layer)); + }, + ); + it.effect( "runs the before/after dumps scoped to auth|storage and the full dump excluding the internal schemas", () => { @@ -868,11 +888,11 @@ describe("migration squash", () => { }); it.effect( - "fails with a health-check timeout when the shadow never becomes healthy, and removes it", + "fails with a health-check timeout when the shadow never becomes connectable, and removes it", () => { // A zero-second health timeout means zero retries after the first failed // probe — an immediate, deterministic timeout with no real/virtual delay. - const s = setup(tmp.current, { neverHealthyShadow: true }); + const s = setup(tmp.current, { neverConnectableShadow: true }); return Effect.gen(function* () { yield* seedMigration(tmp.current, "0_init.sql"); yield* seedMigration(tmp.current, "1_target.sql"); diff --git a/apps/cli/src/commands/notebooks/notebooks.shared.ts b/apps/cli/src/commands/notebooks/notebooks.shared.ts index fb1a0c523d..e58b0e69a4 100644 --- a/apps/cli/src/commands/notebooks/notebooks.shared.ts +++ b/apps/cli/src/commands/notebooks/notebooks.shared.ts @@ -6,6 +6,7 @@ import { canPrompt } from "../../command-internal/hosted-consent/hosted-consent. 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, @@ -124,18 +125,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/secrets/set/SIDE_EFFECTS.md b/apps/cli/src/commands/secrets/set/SIDE_EFFECTS.md index 93be242827..ce5b8a1198 100644 --- a/apps/cli/src/commands/secrets/set/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/secrets/set/SIDE_EFFECTS.md @@ -41,7 +41,7 @@ Target: hosted, consent default No — see [Command target](../../../../docs/com | `SUPABASE_PROFILE` | selects API base URL: `supabase` → `api.supabase.com`, `supabase-staging` → `api.supabase.green`, `supabase-local` → `http://localhost:8080`. May alternatively be a filesystem path to a YAML profile with at least `api_url:` and optional `name:` (used by the cli-e2e test harness). | no (defaults to `supabase`) | | `SUPABASE_PROJECT_ID` | project ref fallback when `--remote=` is unset; also the parent project that `--remote=` is resolved against | no (also reads `/supabase/.temp/project-ref` then prompts on TTY) | | `SUPABASE_YES` | confirms the set without a prompt, same as `--yes`; read from the shell only (a project `.env` value does not count) | -| `SUPABASE_WORKDIR` | base directory for the `.temp/project-ref` lookup | no (walks up from CWD looking for `supabase/config.toml` or `supabase/config.json`) | +| `SUPABASE_WORKDIR` | base directory for the `.temp/project-ref` lookup; when set, also for `supabase/config.*` and its `.env` files, used as-is with no ancestor climb | no (walks up from CWD looking for `supabase/config.toml` or `supabase/config.json`) | | `env(VAR)` references | values matching `env(NAME)` in `[edge_runtime.secrets]` are resolved against the loaded env. Missing variables preserve the literal verbatim. | — | ## Exit Codes diff --git a/apps/cli/src/commands/secrets/set/set.integration.test.ts b/apps/cli/src/commands/secrets/set/set.integration.test.ts index a31cd9d854..0c2bea8032 100644 --- a/apps/cli/src/commands/secrets/set/set.integration.test.ts +++ b/apps/cli/src/commands/secrets/set/set.integration.test.ts @@ -85,10 +85,16 @@ interface SetupOpts { network?: "fail"; env?: Record; confirm?: boolean; + /* Temp-root subdirectories for the process cwd and an explicit `--workdir`. */ + cwd?: string; + workdir?: string; } const tempRoot = useTempWorkdir("supabase-secrets-set-int-"); +const underTemp = (dir: string | undefined) => + dir === undefined ? tempRoot.current : `${tempRoot.current}/${dir}`; + function setup(opts: SetupOpts = {}) { const out = mockOutput({ format: opts.format ?? "text", confirmLogout: opts.confirm }); const api = mockCommandPlatformApi({ @@ -96,7 +102,10 @@ function setup(opts: SetupOpts = {}) { response: { status: opts.status ?? 201, body: null }, network: opts.network, }); - const cliSettings = mockCommandSettings({ workdir: tempRoot.current }); + const cliSettings = mockCommandSettings({ + workdir: underTemp(opts.workdir), + explicitWorkdir: opts.workdir !== undefined, + }); const debugLogger = mockDebugLoggerTracked(); const layer = Layer.mergeAll( buildTestRuntime({ @@ -106,7 +115,7 @@ function setup(opts: SetupOpts = {}) { outputFlag: opts.outputFlag === undefined ? Option.none() : Option.some(opts.outputFlag), tty: mockTty({ stdinIsTty: opts.confirm !== undefined, stdoutIsTty: false }), }), - mockRuntimeInfo({ cwd: tempRoot.current }), + mockRuntimeInfo({ cwd: underTemp(opts.cwd) }), processEnvLayer(opts.env ?? {}), cliConfigValuesTestLayer, debugLogger.layer, @@ -404,6 +413,59 @@ SHARED = "config-shared" }, ); + it.effect("uses the --workdir project's config and .env, not the process cwd's", () => { + const { layer, api } = setup({ cwd: "other", workdir: "app" }); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.makeDirectory(yield* tempPath("other", "supabase"), { recursive: true }); + yield* fs.makeDirectory(yield* tempPath("app", "supabase"), { recursive: true }); + yield* writeTempFile("other/supabase/config.toml", '[edge_runtime.secrets]\nLEAK = "cwd"\n'); + yield* writeTempFile("other/supabase/.env", "TOKEN_VAL=cwd-token\nCWD_ONLY_VAL=cwd-only\n"); + yield* writeTempFile( + "app/supabase/config.toml", + '[edge_runtime.secrets]\nPLAIN = "workdir"\nTOKEN = "env(TOKEN_VAL)"\nCWD_ONLY = "env(CWD_ONLY_VAL)"\n', + ); + yield* writeTempFile("app/supabase/.env", "TOKEN_VAL=workdir-token\n"); + yield* writeTempFile("other/args.env", "FROM_FILE=cwd-relative\n"); + yield* secretsSet({ + remote: Option.none(), + projectRef: Option.none(), + envFile: Option.some("args.env"), + secrets: [], + }); + expect(api.requests[0]?.body).toEqual([ + { name: "PLAIN", value: "workdir" }, + { name: "TOKEN", value: "workdir-token" }, + { name: "FROM_FILE", value: "cwd-relative" }, + ]); + }).pipe(Effect.provide(layer)); + }); + + it.effect("uses neither a parent project nor the cwd project from an explicit --workdir", () => { + const { layer, api } = setup({ cwd: "other", workdir: "app/nested" }); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + yield* fs.makeDirectory(yield* tempPath("app", "nested"), { recursive: true }); + yield* fs.makeDirectory(yield* tempPath("app", "supabase"), { recursive: true }); + yield* fs.makeDirectory(yield* tempPath("other", "supabase"), { recursive: true }); + yield* writeTempFile("app/supabase/config.toml", '[edge_runtime.secrets]\nPARENT = "x"\n'); + yield* writeTempFile("other/supabase/config.toml", '[edge_runtime.secrets]\nLEAK = "cwd"\n'); + const exit = yield* Effect.exit( + secretsSet({ + remote: Option.none(), + projectRef: Option.none(), + envFile: Option.none(), + secrets: [], + }), + ); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.pretty(exit.cause)).toContain("SecretsNoArgumentsError"); + } + expect(api.requests).toHaveLength(0); + }).pipe(Effect.provide(layer)); + }); + it.effect("interpolates env(VAR) in config.toml secrets when the env var is defined", () => { const { layer, api } = setup({ env: { MY_DB_URL: "postgres://x" } }); return Effect.gen(function* () { 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 358be7ec3a..c8c2ed78c1 100644 --- a/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts +++ b/apps/cli/src/commands/seed/buckets/buckets.integration.test.ts @@ -3065,7 +3065,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.merge(layer, projectStateModeLayer("stack"))), diff --git a/apps/cli/src/commands/sso/add/add.handler.ts b/apps/cli/src/commands/sso/add/add.handler.ts index 831c4fc6ba..d65c8e6e88 100644 --- a/apps/cli/src/commands/sso/add/add.handler.ts +++ b/apps/cli/src/commands/sso/add/add.handler.ts @@ -118,6 +118,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 0c3e616d2d..1aa92b957f 100644 --- a/apps/cli/src/commands/sso/list/list.handler.ts +++ b/apps/cli/src/commands/sso/list/list.handler.ts @@ -55,6 +55,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 eab5670f1f..3def95b8d1 100644 --- a/apps/cli/src/commands/sso/remove/remove.handler.ts +++ b/apps/cli/src/commands/sso/remove/remove.handler.ts @@ -53,6 +53,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 42004fa303..24b412ed29 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 }); @@ -285,6 +288,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, { @@ -341,6 +345,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 85c4103a5c..e925019b54 100644 --- a/apps/cli/src/commands/sso/update/update.handler.ts +++ b/apps/cli/src/commands/sso/update/update.handler.ts @@ -166,6 +166,7 @@ const handleGetError = (gate: GateContext, providerId: string, cause: Supa body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), @@ -181,6 +182,7 @@ const handlePutError = (gate: GateContext, cause: SupabaseApiError) => body: mapped.body, message: mapped.message, upgradeSuggested, + entitlement: mapped.entitlement, }); }), ), diff --git a/apps/cli/src/commands/start/SIDE_EFFECTS.md b/apps/cli/src/commands/start/SIDE_EFFECTS.md index 6e4762498e..6cc2ab4cf9 100644 --- a/apps/cli/src/commands/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/start/SIDE_EFFECTS.md @@ -74,6 +74,71 @@ tar archive) → `docker start` sequence is assembled by that shared core, not b health-checked via an HTTP probe through Kong (`/functions/v1/_internal/health`), not a Docker healthcheck — mirroring PostgREST's own probe shape. +### Readiness gating + +Postgres's own wait does not read Docker's health status: Docker runs every `HEALTHCHECK` with +`docker exec`, which some hosts cannot do (nested or sandboxed Docker), and +the status lags connectability by up to one 10s health interval. The wait polls every 500ms +within `db.health_timeout` (`SUPABASE_DB_HEALTH_TIMEOUT`): `docker container inspect` (a +container that is not running fails the wait at once), then a short-lived host connect to the +published Postgres address (2s dial cap). The first successful connect ends the wait. A timeout +reports ` container is not ready: `, after the container's logs. `db +start` runs the same wait, including with `--from-backup`. + +Every other health-checked container is gated on Docker's health status (`healthy`), polled once +per second within the 30s service timeout. A container with no Docker healthcheck passes once +it is running. PostgREST and Edge Runtime keep their HTTP HEAD probes through Kong. The wait also +stops after `(30 + 1) * (1 + 10)` seconds of wall-clock time, so a gateway that accepts +connections and never answers cannot outlast the retry budget; it then fails like an exhausted +budget, with the last round's reasons (`container is not ready: readiness probe hung after s` +when no round completed). + +After that wait, including when `--ignore-health-check` downgrades its failure and when no +health-checked service was started, `start` waits up to 15s for Docker's first health verdict on +the Postgres container (polling `docker container inspect` every 1s until the status leaves +`starting`, the container stops running, or Docker cannot launch its health check), so a +following `supabase status` or `supabase start` does not see `starting`. It never fails the run. + +When a container is running but Docker could not launch its health check, it is judged from the +host instead of failing. This applies only when the container's last recorded probe has exit +code `-1` with an `OCI runtime exec failed ... unable to start container process` output, whatever +else the runtime reports (a cgroup or `setns` failure, for example); a probe timeout, a failing +probe command, a restarting container, and an image fault in the healthcheck command +(`exec: "": executable file not found in $PATH`, `exec: "": stat : no such file +or directory`, `exec : no such file or directory`, `exec : exec format error`, or +`permission denied` in either form) keep failing: + +| Container | Judged by | +| --------------------------------------------------- | --------------------------------------------------------------- | +| kong | `GET /`; any HTTP response counts as up | +| gotrue | `GET /auth/v1/health` through Kong, expecting 200 | +| storage | `GET /storage/v1/status` through Kong, expecting 200 | +| pg-meta | `GET /pg/health` through Kong, expecting 200 | +| logflare | `GET /analytics/v1/health` through Kong, expecting 200 | +| every other container, and all of them without Kong | accepted as soon as it is running; readiness is not verified | + +The gateway requests carry the same `apikey`/`Authorization` headers as the bucket-seeding +requests. Kong answers `404` on `/` because no route matches, which proves its proxy is up; +only a failed connection or the 10s request timeout (covering the response body too) counts as +not ready. If a gateway request keeps failing, the container reports `container is not ready: : ` (Kong: +`container is not ready: `) and the wait times out as usual. + +When at least one container needed this fallback, `start` prints one `WARNING:` line to stderr, +in every output format, after the health wait and before the success summary: +`Docker cannot run health checks inside containers on this host (OCI runtime exec failed).` +followed by `Readiness of was not verified.` (the containers accepted on `running` +alone) or `Readiness was checked from the host instead.` when every fallback was a gateway +request. `cli_stack_started` still fires on such a success. + +A not-ready reason names the last health-check probe Docker recorded: +` container is not ready: (last health check exit code : )`, with +the output's whitespace collapsed and capped at 300 characters, and the `: ` part +omitted when the output is empty. No probe on record leaves the reason as +`container is not ready: `. When a timed-out container's health check could not be +launched, the error's `suggestion` names those containers, says Docker could not run the check +(typically nested or sandboxed Docker), and gives `docker exec true` to confirm; it +is joined after the `exec format error` advice when both apply. + ### Storage bucket seeding (`seedBucketsRun`) Runs only when `isFreshVolume && Storage started`, after the bulk @@ -104,7 +169,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 | @@ -128,7 +193,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 +260,19 @@ version check that _would_ call the Management API and is deliberately not imple ## Environment Variables -| Variable | Purpose | Required? | -| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_*` (any dotted config field) | Overrides the config key at the matching path, `SUPABASE_` plus the upper-snake path (e.g. `SUPABASE_AUTH_ENABLED`, `SUPABASE_API_PORT`). Shell beats project `.env*`, which beats config. An unparsable value fails the command; an empty value is ignored. Env for a key inside an optional section (e.g. an SMS provider) applies only when the section exists in config | 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 (shell → project `.env*` → config → 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 from the ambient shell only (a project `.env`/`.env.`/`.env.local` file never selects the daemon) 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) | Overrides the config key at the matching path, `SUPABASE_` plus the upper-snake path (e.g. `SUPABASE_AUTH_ENABLED`, `SUPABASE_API_PORT`). Shell beats project `.env*`, which beats config. An unparsable value fails the command; an empty value is ignored. Env for a key inside an optional section (e.g. an SMS provider) applies only when the section exists in config | 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 (shell → project `.env*` → config → 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 from the ambient shell only (a project `.env`/`.env.`/`.env.local` file never selects the daemon) 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`. @@ -218,7 +284,7 @@ code is surfaced on failure. | Code | Condition | | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `0` | success — every started container passed its health check | +| `0` | success — every started container passed its health check, or was accepted through the exec-failure fallback with a stderr `WARNING:` (see "Readiness gating") | | `0` | the stack was already running — shows status instead of restarting | | `0` | `--ignore-health-check` set and one or more containers timed out — the failure is printed and swallowed, no rollback | | `1` | `--ignore-health-check` set, the fresh-volume/Storage-healthy recheck-and-seed path ran (see "Storage bucket seeding"), and that seed itself failed — rolls back despite the flag | @@ -232,8 +298,8 @@ code is surfaced on failure. | `1` | image pull exhausted across every registry candidate, or the Docker daemon becomes unreachable during the pre-pull — even with `--ignore-health-check` (never swallowed into exit 0; see the `--ignore-health-check` note under "Notes") | | `1` | Studio is enabled and `/supabase/snippets/` cannot be created (a file is in the way, or the workdir is not writable) — fails before any network or container is created | | `1` | network, volume, container create, or container start failure (including a port conflict) — rolls back everything created so far | -| `1` | health check timeout **without** `--ignore-health-check` — rolls back | -| `1` | Postgres itself fails to start or its own health wait times out, **without** `--ignore-health-check` — rolls back | +| `1` | health check timeout **without** `--ignore-health-check` (including a container whose exec-failure fallback request keeps failing) — rolls back | +| `1` | Postgres itself fails to start or its own readiness wait (inspect + host connect) times out, **without** `--ignore-health-check` — rolls back | | `0` | `--ignore-health-check` set and Postgres's own health wait times out — the failure is printed and swallowed, no rollback; no OTHER service is ever created (Postgres's failure is returned before any other bring-up step runs), but the command still prints "Started..." + the (config-derived) status table | | `1` | fresh-volume DB setup failure (schema SQL / one-shot migrate job / vault upsert / roles seed / migration-apply) — rolls back | | `1` | fresh-volume bucket-seeding failure — rolls back | @@ -279,7 +345,7 @@ output modes. invalid values and listing the 13 valid ones — the command still proceeds. - stderr: already-running banner (`supabase start` in aqua) **or** the bring-up sequence: `Starting database...` (fresh volume) / `Starting database from backup...` (existing - volume) → Postgres create+start+health-wait → `Starting containers...` → (image + volume) → Postgres create+start+readiness wait → `Starting containers...` → (image pre-pull) → per-container create+start → `Waiting for health checks...` → `Started supabase local development setup.` - stderr (conditional, fresh-volume bucket seeding): `Creating bucket: ` per @@ -288,6 +354,8 @@ supabase local development setup.` buckets to prune.` for a bucket left in place. These seeding lines use the raw writer, so they appear in every output mode, not only text. No confirmation question is printed and no stdin line is read. +- stderr (conditional, Docker cannot run health checks on this host): one `WARNING:` line; see + "Readiness gating". Written to stderr in the json and stream-json formats too. - stderr (conditional, health-check timeout): per unhealthy container, a ` container logs:` header and that container's `docker logs` output, then one `: ` line each. Containers are named `supabase__` @@ -311,7 +379,8 @@ buckets to prune.` for a bucket left in place. These seeding lines use the raw w A single JSON object (or a `result` NDJSON event) carrying the same value shape `supabase status --output json` produces. All the progress/warning/banner/security-notice -text above is suppressed in these modes — stdout stays payload-only. On a health-check +text above is suppressed in these modes — stdout stays payload-only — except the exec-failure +`WARNING:` (see "Readiness gating"), which stays on stderr. On a health-check timeout the error payload carries the advice above as a discrete `error.suggestion` string alongside `error.message`, so a caller can surface it without re-parsing the message. It is prose, not structured data. @@ -354,8 +423,11 @@ prose, not structured data. exit 0") over-promises in this scenario — a pre-pull failure is not an "unhealthy service", but a user reading only `--help` may still expect exit 0 here. - `--preview` is a hidden, parsed-but-inert flag. -- The already-running check uses `docker container inspect` on the Postgres container, - not a health check. For a verified +- The already-running check uses `docker container inspect` on the Postgres container and, + unless `--ignore-health-check` is set, requires its Docker health to be `healthy` or absent; + otherwise it fails with ` container is not ready: ...` (reason format in "Readiness + gating"). When Docker could not launch the health check, it accepts the container and prints + the same `WARNING:` naming `supabase_db_` as unverified. For a verified stopped container outside Bitbucket Pipelines, `start` removes all current-project containers — including running siblings — and unused networks, preserves named volumes, and continues normal startup. After container removal succeeds, it deletes @@ -368,7 +440,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.handler.ts b/apps/cli/src/commands/start/start.handler.ts index 3fd98b20cb..197724c2f1 100644 --- a/apps/cli/src/commands/start/start.handler.ts +++ b/apps/cli/src/commands/start/start.handler.ts @@ -32,7 +32,9 @@ import { aqua, yellow } from "../../command-internal/colors.ts"; import { apiTlsCertReadErrorMessage, apiTlsKeyReadErrorMessage, + ConfigValidateError, emailContentPathReadErrorMessage, + fsErrorCause, resolveApiTlsPath, resolveEmailTemplateContentPath, } from "../../command-internal/config-validate.ts"; @@ -55,6 +57,8 @@ import { import { resolveDockerNetworkMode } from "../../shared/functions/functions-docker.ts"; import { supabaseEnvStringWithProjectFallback } from "../../command-internal/supabase-env.ts"; import { + formatNotReadyReason, + healthCheckCannotExec, inspectContainerState, listContainersByLabel, type ContainerIdName, @@ -136,7 +140,10 @@ import { } from "../../command-internal/db-bootstrap/container-lifecycle.ts"; import { ensureImagesCached } from "../../command-internal/db-bootstrap/image-prepull.ts"; import { + awaitDockerHealthSettled, + healthCheckExecUnavailableWarning, waitForHealthyServices, + type ExecFallbackProbe, type HealthCheckPostgrestGateway, type HealthCheckTimeoutError, } from "../../command-internal/db-bootstrap/health-check.ts"; @@ -179,14 +186,13 @@ 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), }); + +function tryStartConfig(thunk: () => T): Effect.Effect { + return Effect.try({ try: thunk, catch: startConfigError }); } const startConfigFailure = (cause: unknown) => @@ -225,14 +231,14 @@ function wrapConfigOverride( * or auth is disabled. */ -function resolveGotrueEnvInput(params: { +const resolveGotrueEnvInput = Effect.fnUntraced(function* (params: { readonly context: LocalResolvedConfigContext; 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, document } = context; @@ -302,9 +308,9 @@ function resolveGotrueEnvInput(params: { passkeyEnabled, webauthn, externalProviders, - signingKeys: resolveConfiguredSigningKeys(config, workdir), - }; -} + signingKeys: yield* resolveConfiguredSigningKeys(config, workdir), + } satisfies Omit; +}); /** * Existence/readability check for one already-resolved `content_path`. Without it, a @@ -352,14 +358,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)) { @@ -427,14 +435,12 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { const context = yield* loadLocalResolvedConfigContext(cliSettings.workdir).pipe( Effect.mapError(startConfigFailure), ); - const values = yield* tryStartConfig(() => - resolveLocalConfigValues( - context.config, - context.hostname, - cliSettings.workdir, - context.document, - ), - ); + const values = yield* resolveLocalConfigValues( + context.config, + context.hostname, + cliSettings.workdir, + context.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. @@ -545,21 +551,14 @@ 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.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.document, + precomputedLocal, + ).pipe(Effect.mapError((cause) => new StatusInvalidConfigError({ message: cause.message }))); const containerIds = statusContainerIds(projectId); const state = gateStatusState(localState, containerIds, excluded); return statusValuesFromState(state, new Map()); @@ -615,9 +614,13 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { }); } if (state.health !== undefined && state.health !== "healthy") { - return yield* new StatusDbNotReadyError({ - message: `${dbContainerId} container is not ready: ${state.health}`, - }); + if (!healthCheckCannotExec(state)) { + return yield* new StatusDbNotReadyError({ + message: `${dbContainerId} ${formatNotReadyReason(state)}`, + }); + } + yield* Effect.annotateCurrentSpan("db.health_exec_unavailable", true); + yield* output.raw(healthCheckExecUnavailableWarning([dbContainerId]), "stderr"); } } @@ -825,7 +828,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) => @@ -835,7 +838,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) => @@ -974,14 +977,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.mapError(startConfigError)), }), }; @@ -1272,6 +1275,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // `docker create` returns) so an unhealthy container reports as `supabase_auth_demo` // rather than 64 hex characters, and the watch list stays in sync with its images. const started = new Map(); + const execFallbacks = new Map(); let postgrestGateway: HealthCheckPostgrestGateway | undefined; let edgeRuntimeGateway: HealthCheckPostgrestGateway | undefined; let storageContainerId: string | undefined; @@ -1374,6 +1378,16 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { yield* createContainer(spawner, spec, startOpts); if (excludeFromHealthWatch !== true) { started.set(spec.containerName, spec.image); + // Routed services fall back to `running` when Kong is excluded. + const gateway = { apiExternalUrl: values.apiUrl, secretKey: values.secretKey }; + execFallbacks.set( + spec.containerName, + entry.gatewayReachable === true + ? { kind: "gateway-reachable", gateway } + : entry.gatewayReadyPath !== undefined && gates.kong + ? { kind: "gateway", gateway, path: entry.gatewayReadyPath } + : { kind: "running" }, + ); } if (entry.service === "postgrest") { postgrestGateway = { @@ -1393,6 +1407,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { postgrestGateway, edgeRuntimeGateway, storageContainerId, + execFallbacks, }; }).pipe( // The rollback's `NoBackupVolume` value is `true` only when this run's Postgres volume was @@ -1448,7 +1463,8 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { // Only reached when Postgres itself became healthy — the `postgresUnhealthyIgnored` // short-circuit above skips this entire block and falls through to the tail below instead. if (bringUpResult.kind === "started") { - const { started, postgrestGateway, edgeRuntimeGateway, storageContainerId } = bringUpResult; + const { started, postgrestGateway, edgeRuntimeGateway, storageContainerId, execFallbacks } = + bringUpResult; // Wraps steps 9-11 below in the same `Effect.onError` rollback `bringUp`'s own pipe uses // above, so a SIGINT/SIGTERM landing anywhere in this tail also rolls back. Per-step manual @@ -1495,6 +1511,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { postgrest: postgrestGateway, edgeRuntime: edgeRuntimeGateway, images: started, + execFallbacks, }), ).pipe(Effect.result); if (Result.isFailure(healthResult)) { @@ -1512,6 +1529,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { const storageHealthResult = yield* withLocalKongCa( waitForHealthyServices(spawner, [storageContainerId], { images: started, + execFallbacks, }), ).pipe(Effect.result); if (Result.isSuccess(storageHealthResult)) { @@ -1530,12 +1548,18 @@ 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; } } } // Downgrade to a warning and fall through to the success path, no rollback. yield* output.raw(`${healthWarningText(error)}\n`, "stderr"); + if (error.readinessAssumed !== undefined) { + yield* output.raw( + healthCheckExecUnavailableWarning(error.readinessAssumed), + "stderr", + ); + } } else { // No manual `rollbackStart` here — the outer `Effect.onError` // below rolls back on this failure too. @@ -1543,6 +1567,15 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) { } } + if (Result.isSuccess(healthResult) && healthResult.success.execUnavailable.length > 0) { + yield* output.raw( + healthCheckExecUnavailableWarning(healthResult.success.readinessAssumed), + "stderr", + ); + } + + yield* awaitDockerHealthSettled(spawner, dbContainerId); + // 10. Storage-bucket seeding, gated on a freshly created volume and Storage having // started. Reached only on a genuine health-check success — mutually exclusive with the // narrower storage-only recheck-and-seed path above, which only runs when this guard is diff --git a/apps/cli/src/commands/start/start.integration.test.ts b/apps/cli/src/commands/start/start.integration.test.ts index 1116bf8157..3dcfa300d2 100644 --- a/apps/cli/src/commands/start/start.integration.test.ts +++ b/apps/cli/src/commands/start/start.integration.test.ts @@ -4,6 +4,7 @@ import { BunServices } from "@effect/platform-bun"; import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest"; import { Cause, + Clock, Deferred, Effect, Exit, @@ -21,6 +22,7 @@ import { import { ChildProcess, ChildProcessSpawner } from "effect/process"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; +import * as TestClock from "effect/testing/TestClock"; import { assert, vi } from "vitest"; import { @@ -41,6 +43,11 @@ import { withEmptyConfigEnv, withEnvVar, } from "../../../tests/helpers/command-mocks.ts"; +import { + EXEC_FAILED_STATE, + PROBE_TIMEOUT_OUTPUT, + inspectedState, +} from "../../../tests/helpers/docker-health.ts"; import { assertPermissionDrift } from "../../../tests/helpers/permission-drift.ts"; import { unusedStackServices } from "../../../tests/helpers/unused-stack.ts"; import { CliArgs } from "../../shared/cli/cli-args.service.ts"; @@ -55,6 +62,7 @@ import { CommandPlatformApiFactory } from "../../auth/command-platform-api-facto import { serviceContainerIds, serviceContainerName } from "../../command-internal/docker-ids.ts"; import { bindMountSpecSource } from "../../command-internal/docker-bind-classify.ts"; import { HealthCheckTimeoutSeconds } from "../../command-internal/db-bootstrap/health-check.ts"; +import { DbConnectError } from "../../command-internal/db-connection.errors.ts"; import { DbConnection, type DbSession } from "../../command-internal/db-connection.service.ts"; import { dockerRunLayer } from "../../command-internal/docker-run.layer.ts"; import { START_EXCLUDABLE_KEYS } from "./start.exclude.ts"; @@ -291,6 +299,75 @@ function defaultRoute(opts: { readonly neverHealthy?: ReadonlySet } = {} }; } +const refusedDbConnect = () => + Effect.fail(new DbConnectError({ message: "failed to connect: connection refused" })); + +/** Docker cannot launch any HEALTHCHECK on this host, Kong's included. */ +function execBrokenHostRoute(base: (args: ReadonlyArray) => RouteResult = defaultRoute()) { + return (args: ReadonlyArray): RouteResult => { + const result = base(args); + if (args[0] === "container" && args[1] === "inspect" && result.stdout?.[0] === HEALTHY_STATE) { + return { stdout: [EXEC_FAILED_STATE] }; + } + return result; + }; +} + +/** + * A db whose Docker health reads `starting` until `healthyAtSeconds` of test-clock time. + * `settleStarted` fires on the first db inspect after the host connect probe succeeded; + * `settled` fires when Docker first reports healthy. + */ +function dbHealthVerdictAt(healthyAtSeconds: number) { + const base = defaultRoute(); + const settleStarted = Deferred.makeUnsafe(); + const settled = Deferred.makeUnsafe(); + let connected = false; + const dbConnect = () => { + connected = true; + return Effect.succeed(fakeDbSession().session); + }; + const route = (args: ReadonlyArray): RouteResult | Effect.Effect => { + const result = base(args); + const isDbInspect = + args[0] === "container" && + args[1] === "inspect" && + (args[2] ?? "").includes("_db_") && + result.stdout?.[0] === HEALTHY_STATE; + if (!isDbInspect) return result; + if (connected) Deferred.doneUnsafe(settleStarted, Effect.void); + return Effect.map(Clock.currentTimeMillis, (now) => { + if (now < healthyAtSeconds * 1000) return { stdout: [STARTING_STATE] }; + Deferred.doneUnsafe(settled, Effect.void); + return result; + }); + }; + return { route, dbConnect, settleStarted, settled }; +} + +/** Answers every Kong-gateway request with the status scripted for its path (200 otherwise), recording each. */ +function mockGatewayHttpClient(statusByPath: Readonly> = {}) { + const requests: Array<{ + readonly method: string; + readonly path: string; + readonly headers: Readonly>; + }> = []; + const layer = Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => { + const { pathname } = new URL(request.url); + requests.push({ method: request.method, path: pathname, headers: { ...request.headers } }); + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response("{}", { status: statusByPath[pathname] ?? 200 }), + ), + ); + }), + ); + return { layer, requests }; +} + /** A `HttpClient` that always answers 200 — the default config's PostgREST/Kong readiness probe. */ const alwaysReadyHttpClientLayer = Layer.succeed( HttpClient.HttpClient, @@ -494,6 +571,8 @@ interface SetupOpts { readonly networkId?: Option.Option; /** `--experimental`/`SUPABASE_EXPERIMENTAL`. Defaults to `false`. */ readonly experimental?: boolean; + /** Replaces the `DbConnection.connect` behind the Postgres readiness probe. Defaults to succeeding. */ + readonly dbConnect?: (typeof DbConnection)["Service"]["connect"]; } const setup = Effect.fnUntraced(function* (opts: SetupOpts = {}) { @@ -523,10 +602,11 @@ const setup = Effect.fnUntraced(function* (opts: SetupOpts = {}) { analytics.layer, child.layer, opts.httpClientLayer ?? alwaysReadyHttpClientLayer, - // Only exercised by a fresh-volume scenario; every other scenario's default route never - // reaches `startSetupLocalDatabase`/`seedBucketsRun`, but both are part of `start`'s - // aggregate Effect type, so every scenario still needs this satisfied. - Layer.succeed(DbConnection, { connect: () => Effect.succeed(dbSession.session) }), + // Backs the Postgres readiness probe on every scenario, and `startSetupLocalDatabase`/ + // `seedBucketsRun` on fresh-volume ones. + Layer.succeed(DbConnection, { + connect: opts.dbConnect ?? (() => Effect.succeed(dbSession.session)), + }), // `Layer.mergeAll` never cross-wires sibling requirements, so `dockerRunLayer` needs // `ChildProcessSpawner`/`ProcessControl` provided to it explicitly, not just present // elsewhere in this merge. @@ -3189,22 +3269,19 @@ content_path = "./supabase/templates/custom_notice.html" // rollback being wired via `Effect.onError` (not `Effect.tapError`, which never sees a // pure interrupt's `Cause`). // - // Marking `db` never-healthy keeps `bringUp`'s Postgres health-check wait suspended in - // its retry loop, rather than the whole synchronous mock bring-up completing before - // this test's probe wait is even scheduled — which would make `Fiber.interrupt` a - // no-op on an already-succeeded fiber. - const neverHealthy = new Set(); - const route = defaultRoute({ neverHealthy }); + // A refused Postgres connect keeps `bringUp`'s readiness wait suspended in its retry + // loop, rather than the whole synchronous mock bring-up completing before this test's + // probe wait is even scheduled — which would make `Fiber.interrupt` a no-op on an + // already-succeeded fiber. + const route = defaultRoute(); let dbContainerId: string | undefined; const dbProbed = yield* Deferred.make(); const { layer, child } = yield* setup({ + dbConnect: refusedDbConnect, route: (args) => { if (args[0] === "create") { const name = containerNameFromCreateArgs(args); - if (name.includes("_db_")) { - neverHealthy.add(name); - dbContainerId = name; - } + if (name.includes("_db_")) dbContainerId = name; } if ( dbContainerId !== undefined && @@ -3221,8 +3298,8 @@ content_path = "./supabase/templates/custom_notice.html" Effect.provide(layer), Effect.forkChild({ startImmediately: true }), ); - // Wait until the health check has actually probed the never-healthy `db` container, - // proving the fiber is suspended inside the retry loop, not merely past `create`. + // Wait until the readiness wait has actually inspected the `db` container, proving the + // fiber is suspended inside the retry loop, not merely past `create`. yield* Deferred.await(dbProbed); // `Fiber.interrupt` only resolves once the target fiber (and its finalizers, // including the `Effect.onError` rollback) has fully completed. @@ -3439,18 +3516,9 @@ content_path = "./supabase/templates/custom_notice.html" () => Effect.gen(function* () { // A zero `db.health_timeout` limits Postgres's wait to a single probe. - const neverHealthy = new Set(); - const base = defaultRoute({ neverHealthy }); - const route = (args: ReadonlyArray): RouteResult => { - if (args[0] === "create") { - const name = containerNameFromCreateArgs(args); - if (name.includes("_db_")) neverHealthy.add(name); - } - return base(args); - }; const { layer, child } = yield* setup({ configContents: 'project_id = "demo"\n[db]\nhealth_timeout = "0s"\n', - route, + dbConnect: refusedDbConnect, }); const exit = yield* Effect.exit(start(flags()).pipe(Effect.provide(layer))); @@ -3472,13 +3540,8 @@ content_path = "./supabase/templates/custom_notice.html" // `ignoreHealthCheck && isUnhealthyStartError(err)` downgrades uniformly to whatever the // run returns, including Postgres's own health-wait failure, which propagates before // any other service is even created. - const neverHealthy = new Set(); - const base = defaultRoute({ neverHealthy }); + const base = defaultRoute(); const route = (args: ReadonlyArray): RouteResult => { - if (args[0] === "create") { - const name = containerNameFromCreateArgs(args); - if (name.includes("_db_")) neverHealthy.add(name); - } // Postgres's own health wait builds its `images` map separately from the bulk one, // so this scripts the marker here too. if (args[0] === "logs" && (args[1] ?? "").includes("_db_")) { @@ -3488,6 +3551,7 @@ content_path = "./supabase/templates/custom_notice.html" }; const { layer, out, child, analytics } = yield* setup({ configContents: 'project_id = "demo"\n[db]\nhealth_timeout = "0s"\n', + dbConnect: refusedDbConnect, route, }); @@ -3786,6 +3850,291 @@ content_path = "./supabase/templates/custom_notice.html" ); }); + describe("Docker's first health verdict on the database", () => { + const runWithClock = ( + effect: Effect.Effect, + db: ReturnType, + ) => + Effect.gen(function* () { + const fiber = yield* effect.pipe(Effect.forkChild({ startImmediately: true })); + yield* Effect.raceFirst(Deferred.await(db.settleStarted), Fiber.await(fiber)); + // Small steps: each settle poll needs a few scheduler turns before it registers its next sleep. + for (let step = 0; step < 160 && fiber.pollUnsafe() === undefined; step++) { + if (yield* Deferred.isDone(db.settled)) break; + yield* TestClock.adjust("100 millis"); + } + return yield* Fiber.await(fiber); + }); + + it.effect( + "start waits for it, so a start right afterwards finds the database ready instead of starting", + () => + Effect.gen(function* () { + const db = dbHealthVerdictAt(5); + const { layer, out } = yield* setup({ route: db.route, dbConnect: db.dbConnect }); + + const first = yield* runWithClock(start(flags()).pipe(Effect.provide(layer)), db); + yield* start(flags()).pipe(Effect.provide(layer)); + + expect(Exit.isSuccess(first)).toBe(true); + expect(out.stderrText).toContain("local development setup is running."); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.effect("start still completes without rolling back when Docker never settles", () => + Effect.gen(function* () { + const db = dbHealthVerdictAt(Number.POSITIVE_INFINITY); + const { layer, child } = yield* setup({ route: db.route, dbConnect: db.dbConnect }); + + const exit = yield* runWithClock(start(flags()).pipe(Effect.provide(layer)), db); + + expect(Exit.isSuccess(exit)).toBe(true); + expect(rollbackWasAttempted(child.spawned)).toBe(false); + }).pipe(Effect.provide(BunServices.layer)), + ); + }); + + describe("a host where Docker cannot exec health checks", () => { + const projectId = "demo"; + const kongRouted = ["auth", "storage", "pg_meta", "analytics"]; + const gatewayVerified = ["kong", ...kongRouted]; + const readinessAssumed = ["realtime", "studio", "inbucket", "vector"]; + + for (const format of ["text", "json"] as const) { + it.effect( + `starts with one stderr warning naming only unverified services in ${format} mode`, + () => + Effect.gen(function* () { + const { layer, out, analytics } = yield* setup({ + format, + route: execBrokenHostRoute(), + httpClientLayer: mockGatewayHttpClient().layer, + }); + + yield* start(flags()).pipe(Effect.provide(layer)); + + const warnings = out.stderrText + .split("\n") + .filter((line) => line.includes("Docker cannot run health checks")); + expect(warnings).toHaveLength(1); + for (const service of readinessAssumed) { + expect(warnings[0]).toContain(serviceContainerName(service, projectId)); + } + for (const service of gatewayVerified) { + expect(warnings[0]).not.toContain(serviceContainerName(service, projectId)); + } + expect(out.stdoutText).not.toContain("Docker cannot run health checks"); + expect(analytics.captured).toContainEqual( + expect.objectContaining({ event: "cli_stack_started" }), + ); + if (format === "json") { + expect(out.messages.find((m) => m.type === "success")?.data).toMatchObject({ + DB_URL: expect.any(String), + }); + } + }).pipe(Effect.provide(BunServices.layer)), + ); + } + + it.effect( + "verifies Kong and gateway-routed services with an authenticated GET, accepting any status from Kong", + () => + Effect.gen(function* () { + const gateway = mockGatewayHttpClient({ "/": 404 }); + const { layer } = yield* setup({ + route: execBrokenHostRoute(), + httpClientLayer: gateway.layer, + }); + + yield* start(flags()).pipe(Effect.provide(layer)); + + const gets = gateway.requests.filter((request) => request.method === "GET"); + expect(gets.map((request) => request.path).toSorted()).toEqual([ + "/", + "/analytics/v1/health", + "/auth/v1/health", + "/pg/health", + "/storage/v1/status", + ]); + for (const request of gets) { + expect(request.headers["apikey"]).toEqual(expect.any(String)); + } + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.effect( + "with Kong excluded, accepts Kong-routed services as running and lists them as unverified", + () => + Effect.gen(function* () { + const unroutable = { + "/auth/v1/health": 503, + "/storage/v1/status": 503, + "/pg/health": 503, + "/analytics/v1/health": 503, + }; + const { layer, out } = yield* setup({ + route: execBrokenHostRoute(), + httpClientLayer: mockGatewayHttpClient(unroutable).layer, + }); + + yield* start(flags({ exclude: ["kong"] })).pipe(Effect.provide(layer)); + + const warnings = out.stderrText + .split("\n") + .filter((line) => line.includes("Docker cannot run health checks")); + expect(warnings).toHaveLength(1); + for (const service of [...kongRouted, ...readinessAssumed]) { + expect(warnings[0]).toContain(serviceContainerName(service, projectId)); + } + expect(warnings[0]).not.toContain(serviceContainerName("kong", projectId)); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.live( + "fails with the gateway status, the exec suggestion and a rollback when Storage is not routable", + () => + Effect.gen(function* () { + const { layer, out, child } = yield* setup({ + route: execBrokenHostRoute(), + httpClientLayer: mockGatewayHttpClient({ "/storage/v1/status": 503 }).layer, + }); + + const error = yield* start(flags()).pipe(Effect.provide(layer), Effect.flip); + + const storage = serviceContainerName("storage", projectId); + expect(error).toMatchObject({ + _tag: "HealthCheckTimeoutError", + message: `${storage} container is not ready: /storage/v1/status: unexpected status 503`, + suggestion: expect.stringContaining(`Check with: docker exec ${storage} true`), + }); + expect(out.stderrText).not.toContain("Docker cannot run health checks"); + expect(out.stderrText).not.toContain("Started"); + expect(rollbackWasAttempted(child.spawned)).toBe(true); + }).pipe( + Effect.provideService(HealthCheckTimeoutSeconds, 0), + Effect.provide(BunServices.layer), + ), + 10_000, + ); + + it.live( + "--ignore-health-check still names the services accepted as running when it downgrades a failure", + () => + Effect.gen(function* () { + const { layer, out, child } = yield* setup({ + route: execBrokenHostRoute(), + httpClientLayer: mockGatewayHttpClient({ "/storage/v1/status": 503 }).layer, + }); + + yield* start(flags({ ignoreHealthCheck: true })).pipe(Effect.provide(layer)); + + const storage = serviceContainerName("storage", projectId); + expect(out.stderrText).toContain( + `${storage} container is not ready: /storage/v1/status: unexpected status 503`, + ); + const warnings = out.stderrText + .split("\n") + .filter((line) => line.includes("Docker cannot run health checks")); + expect(warnings).toHaveLength(1); + for (const service of readinessAssumed) { + expect(warnings[0]).toContain(serviceContainerName(service, projectId)); + } + expect(warnings[0]).not.toContain(storage); + expect(rollbackWasAttempted(child.spawned)).toBe(false); + }).pipe( + Effect.provideService(HealthCheckTimeoutSeconds, 0), + Effect.provide(BunServices.layer), + ), + 10_000, + ); + + it.live( + "does not accept a Docker probe timeout as an exec failure", + () => + Effect.gen(function* () { + const timedOut = inspectedState({ + health: "unhealthy", + probes: [{ exitCode: -1, output: PROBE_TIMEOUT_OUTPUT }], + }); + const base = defaultRoute(); + const { layer } = yield* setup({ + route: (args) => { + const result = base(args); + return args[0] === "container" && + args[1] === "inspect" && + (args[2] ?? "").includes("_studio_") && + result.stdout?.[0] === HEALTHY_STATE + ? { stdout: [timedOut] } + : result; + }, + }); + + const error = yield* start(flags()).pipe(Effect.provide(layer), Effect.flip); + + expect(error).toMatchObject({ + _tag: "HealthCheckTimeoutError", + message: `${serviceContainerName("studio", projectId)} container is not ready: unhealthy (last health check exit code -1: Health check exceeded timeout (2s))`, + }); + expect(error).not.toHaveProperty("suggestion"); + }).pipe( + Effect.provideService(HealthCheckTimeoutSeconds, 0), + Effect.provide(BunServices.layer), + ), + 10_000, + ); + + it.effect( + "already running: accepts a db whose health check cannot exec, with a stderr warning", + () => + Effect.gen(function* () { + const { layer, out, child } = yield* setup({ + route: (args) => { + if (args[0] === "container" && args[1] === "inspect") { + return { stdout: [EXEC_FAILED_STATE] }; + } + if (args[0] === "ps") return { stdout: [] }; + return { exitCode: 0 }; + }, + }); + + yield* start(flags()).pipe(Effect.provide(layer)); + + expect(out.stderrText).toContain("WARNING:"); + expect(out.stderrText).toContain("Readiness of supabase_db_demo was not verified."); + expect(child.spawned.some((s) => s.args[0] === "create")).toBe(false); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.effect("already running: still rejects a db whose health check ran and failed", () => + Effect.gen(function* () { + const { layer, out } = yield* setup({ + route: (args) => { + if (args[0] === "container" && args[1] === "inspect") { + return { + stdout: [ + inspectedState({ + health: "unhealthy", + probes: [{ exitCode: 1, output: "pg_isready: no response" }], + }), + ], + }; + } + return { exitCode: 0 }; + }, + }); + + const error = yield* start(flags()).pipe(Effect.provide(layer), Effect.flip); + + expect(error).toMatchObject({ + _tag: "StatusDbNotReadyError", + message: + "supabase_db_demo container is not ready: unhealthy (last health check exit code 1: pg_isready: no response)", + }); + expect(out.stderrText).not.toContain("Docker cannot run health checks"); + }).pipe(Effect.provide(BunServices.layer)), + ); + }); + describe("--network-id", () => { it.effect("overrides the generated network name and every container's --network flag", () => Effect.gen(function* () { @@ -5208,16 +5557,7 @@ content_path = "./supabase/templates/custom_notice.html" "SUPABASE_DB_HEALTH_TIMEOUT", "0s", Effect.gen(function* () { - const neverHealthy = new Set(); - const base = defaultRoute({ neverHealthy }); - const route = (args: ReadonlyArray): RouteResult => { - if (args[0] === "create") { - const name = containerNameFromCreateArgs(args); - if (name.includes("_db_")) neverHealthy.add(name); - } - return base(args); - }; - const { layer, child } = yield* setup({ route }); + const { layer, child } = yield* setup({ dbConnect: refusedDbConnect }); const exit = yield* Effect.exit(start(flags()).pipe(Effect.provide(layer))); expect(Exit.isFailure(exit)).toBe(true); diff --git a/apps/cli/src/commands/start/start.services.ts b/apps/cli/src/commands/start/start.services.ts index cf27000ec0..6f117303e4 100644 --- a/apps/cli/src/commands/start/start.services.ts +++ b/apps/cli/src/commands/start/start.services.ts @@ -22,6 +22,10 @@ export interface StartServiceMeta { * Logflare; Studio waits on pg-meta). Not a full dependency graph, just a sequencing hint. */ readonly dependsOn?: ReadonlyArray; + /** The Kong route used to verify readiness when Docker cannot exec the health check. */ + readonly gatewayReadyPath?: string; + /** The service is the gateway itself: any HTTP response from it proves it is up. */ + readonly gatewayReachable?: true; } export interface StartServiceEntry extends ServiceCatalogEntry, StartServiceMeta {} @@ -32,7 +36,14 @@ export interface StartServiceEntry extends ServiceCatalogEntry, StartServiceMeta */ const START_SERVICE_META_BY_SERVICE: ReadonlyMap = new Map([ ["postgres", { imageConfigField: "db.image", enabledGate: "always" }], - ["logflare", { imageConfigField: "analytics.image", enabledGate: "analytics.enabled" }], + [ + "logflare", + { + imageConfigField: "analytics.image", + enabledGate: "analytics.enabled", + gatewayReadyPath: "/analytics/v1/health", + }, + ], [ "vector", { @@ -41,12 +52,26 @@ const START_SERVICE_META_BY_SERVICE: ReadonlyMap = new dependsOn: ["logflare"], }, ], - ["kong", { imageConfigField: "api.kong_image", enabledGate: "none" }], - ["gotrue", { imageConfigField: "auth.image", enabledGate: "auth.enabled" }], + ["kong", { imageConfigField: "api.kong_image", enabledGate: "none", gatewayReachable: true }], + [ + "gotrue", + { + imageConfigField: "auth.image", + enabledGate: "auth.enabled", + gatewayReadyPath: "/auth/v1/health", + }, + ], ["mailpit", { imageConfigField: "local_smtp.image", enabledGate: "local_smtp.enabled" }], ["realtime", { imageConfigField: "realtime.image", enabledGate: "realtime.enabled" }], ["postgrest", { imageConfigField: "api.image", enabledGate: "api.enabled" }], - ["storage", { imageConfigField: "storage.image", enabledGate: "storage.enabled" }], + [ + "storage", + { + imageConfigField: "storage.image", + enabledGate: "storage.enabled", + gatewayReadyPath: "/storage/v1/status", + }, + ], [ "imgproxy", { @@ -56,7 +81,14 @@ const START_SERVICE_META_BY_SERVICE: ReadonlyMap = new }, ], ["edgeRuntime", { imageConfigField: "edge_runtime.image", enabledGate: "edge_runtime.enabled" }], - ["pgMeta", { imageConfigField: "studio.pgmeta_image", enabledGate: "studio.enabled" }], + [ + "pgMeta", + { + imageConfigField: "studio.pgmeta_image", + enabledGate: "studio.enabled", + gatewayReadyPath: "/pg/health", + }, + ], [ "studio", { @@ -84,5 +116,7 @@ export const START_SERVICES: ReadonlyArray = SERVICE_CATALOG. imageConfigField: meta?.imageConfigField ?? "", enabledGate: meta?.enabledGate ?? "", dependsOn: meta?.dependsOn, + gatewayReadyPath: meta?.gatewayReadyPath, + gatewayReachable: meta?.gatewayReachable, }; }); 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 35d105bf7d..3109aa786d 100644 --- a/apps/cli/src/commands/start/start.services.unit.test.ts +++ b/apps/cli/src/commands/start/start.services.unit.test.ts @@ -110,6 +110,7 @@ describe("startServiceMeta", () => { imageConfigField: entry?.imageConfigField, enabledGate: entry?.enabledGate, dependsOn: entry?.dependsOn, + gatewayReadyPath: entry?.gatewayReadyPath, }); }); 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 1158b9129b..590ec1ea07 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"; @@ -112,6 +116,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); @@ -304,7 +313,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/", { @@ -377,15 +390,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/commands/status/SIDE_EFFECTS.md b/apps/cli/src/commands/status/SIDE_EFFECTS.md index deceb29df8..8d7b2bda3a 100644 --- a/apps/cli/src/commands/status/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/status/SIDE_EFFECTS.md @@ -90,7 +90,8 @@ convention used elsewhere in config loading, and take precedence over the corres | `1` | listing running containers failed (Docker daemon unreachable, etc.) | | `1` | the db container inspect call failed (including "not found") — health assertion, skipped by `--ignore-health-check` above | | `1` | the db container is present but not in the `running` state — health assertion, skipped by `--ignore-health-check` above | -| `1` | the db container is running but its Docker health check isn't `healthy` — health assertion, skipped by `--ignore-health-check` above | +| `1` | the db container is running but its Docker health check isn't `healthy` — health assertion, skipped by `--ignore-health-check` above; the message ends with the last recorded probe, `(last health check exit code : )`, when Docker has one | +| `0` | the db container is running but Docker could not launch its health check (last probe exit code `-1`, `OCI runtime exec failed`, as on nested or sandboxed Docker) — status renders, with a stderr `WARNING:` | | `1` | `auth.jwt_secret` is configured but shorter than 16 characters (rejected at config-load time) | | `1` | `auth.signing_keys_path` is configured but the file is missing/malformed, or its first key's algorithm is not `RS256`/`ES256` | | `1` | `api.enabled` and `api.tls.enabled` are true and only one of `api.tls.cert_path`/`key_path` is set (rejected at config-load time) | @@ -212,6 +213,10 @@ headers); colors are stripped on non-TTY/piped output. bracketed list, e.g. `[supabase_storage_test supabase_studio_test]`) whenever one of the 13 expected service containers isn't in the running set. +When Docker could not launch the db container's health check, stderr gets `WARNING: Docker cannot +run health checks inside containers on this host (OCI runtime exec failed). Readiness of + was not verified.` in every output format, and the status still renders on stdout. + ### `-o env` `KEY="VALUE"` lines (unquoted for integer-looking values), one per resolved field, sorted by diff --git a/apps/cli/src/commands/status/status.handler.ts b/apps/cli/src/commands/status/status.handler.ts index 7d90f339b1..f1f90ab4a6 100644 --- a/apps/cli/src/commands/status/status.handler.ts +++ b/apps/cli/src/commands/status/status.handler.ts @@ -12,7 +12,10 @@ import { serviceContainerIds, localDbContainerId, } from "../../command-internal/docker-ids.ts"; +import { healthCheckExecUnavailableWarning } from "../../command-internal/db-bootstrap/health-check.ts"; import { + formatNotReadyReason, + healthCheckCannotExec, inspectContainerState, listContainersByLabel, } from "../../command-internal/docker-lifecycle.ts"; @@ -160,22 +163,19 @@ 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.resolvedConfig.loaded.document, - ), - catch: (cause) => + const localState = yield* resolveStatusLocalState( + context.config, + context.hostname, + cliSettings.workdir, + context.projectEnvValues, + context.resolvedConfig.loaded.document, + ).pipe( + Effect.mapError((cause) => cause instanceof CliConfigValueError ? cause - : new StatusInvalidConfigError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); + : new StatusInvalidConfigError({ message: cause.message }), + ), + ); // 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 @@ -204,9 +204,13 @@ export const status = Effect.fn("status")(function* (flags: StatusFlags) { }); } if (state.health !== undefined && state.health !== "healthy") { - return yield* new StatusDbNotReadyError({ - message: `${dbContainerId} container is not ready: ${state.health}`, - }); + if (!healthCheckCannotExec(state)) { + return yield* new StatusDbNotReadyError({ + message: `${dbContainerId} ${formatNotReadyReason(state)}`, + }); + } + yield* Effect.annotateCurrentSpan("db.health_exec_unavailable", true); + yield* output.raw(healthCheckExecUnavailableWarning([dbContainerId]), "stderr"); } } @@ -245,7 +249,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/status/status.integration.test.ts b/apps/cli/src/commands/status/status.integration.test.ts index de74e48292..37f4a2dee4 100644 --- a/apps/cli/src/commands/status/status.integration.test.ts +++ b/apps/cli/src/commands/status/status.integration.test.ts @@ -39,6 +39,11 @@ import { useTempWorkdir, withEnvVar, } from "../../../tests/helpers/command-mocks.ts"; +import { + EXEC_FAILED_STATE, + PROBE_TIMEOUT_OUTPUT, + inspectedState, +} from "../../../tests/helpers/docker-health.ts"; import { assertPermissionDrift } from "../../../tests/helpers/permission-drift.ts"; import type { CommandPlatformApiFactoryError } from "../../auth/command-platform-api-factory.service.ts"; import { CommandPlatformApiFactory } from "../../auth/command-platform-api-factory.service.ts"; @@ -1026,6 +1031,39 @@ content_path = "./supabase/templates/password_changed_notification.html" }).pipe(Effect.provide(layer)); }); + it.effect("reports a db whose health check cannot exec as running, with a stderr warning", () => { + const { layer, out } = setup({ route: defaultRoute({ dbInspectStdout: EXEC_FAILED_STATE }) }); + return Effect.gen(function* () { + yield* writeConfig(); + yield* status(flags()); + expect(out.stderrText).toContain("WARNING:"); + expect(out.stderrText).toContain("Readiness of supabase_db_demo was not verified."); + expect(out.stdoutText).toContain("⛁ Database"); + expect(out.stdoutText).not.toContain("WARNING"); + }).pipe(Effect.provide(layer)); + }); + + it.effect("still fails a db whose last health probe timed out, naming the probe", () => { + const { layer, out } = setup({ + route: defaultRoute({ + dbInspectStdout: inspectedState({ + health: "unhealthy", + probes: [{ exitCode: -1, output: PROBE_TIMEOUT_OUTPUT }], + }), + }), + }); + return Effect.gen(function* () { + yield* writeConfig(); + const error = yield* status(flags()).pipe(Effect.flip); + expect(error).toMatchObject({ + _tag: "StatusDbNotReadyError", + message: + "supabase_db_demo container is not ready: unhealthy (last health check exit code -1: Health check exceeded timeout (2s))", + }); + expect(out.stderrText).not.toContain("Docker cannot run health checks"); + }).pipe(Effect.provide(layer)); + }); + it.effect("fails when db inspect errors for a reason other than not-found", () => { const { layer } = setup({ route: defaultRoute({ dbInspectExitCode: 1, dbInspectStderr: ["permission denied"] }), @@ -1337,15 +1375,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/stop/SIDE_EFFECTS.md b/apps/cli/src/commands/stop/SIDE_EFFECTS.md index bbf05f5e72..09df6da0f0 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 3504fafd1f..ff16d14962 100644 --- a/apps/cli/src/commands/stop/stop.handler.ts +++ b/apps/cli/src/commands/stop/stop.handler.ts @@ -66,23 +66,20 @@ 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.resolvedConfig.loaded.document, - ), - catch: (cause) => + yield* resolveLocalConfigValues( + context.config, + context.hostname, + cliSettings.workdir, + context.resolvedConfig.loaded.document, + ).pipe( + Effect.mapError((cause) => cause instanceof CliConfigValueError ? cause - : new StopConfigLoadError({ - message: cause instanceof Error ? cause.message : String(cause), - }), - }); + : new StopConfigLoadError({ message: cause.message }), + ), + ); return context.projectId; }); diff --git a/apps/cli/src/commands/unlink/unlink.integration.test.ts b/apps/cli/src/commands/unlink/unlink.integration.test.ts index fb740fc2e0..f57f3273ae 100644 --- a/apps/cli/src/commands/unlink/unlink.integration.test.ts +++ b/apps/cli/src/commands/unlink/unlink.integration.test.ts @@ -386,14 +386,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/vanity-subdomains/activate/activate.handler.ts b/apps/cli/src/commands/vanity-subdomains/activate/activate.handler.ts index 96b62b12b3..93d9aa98a9 100644 --- a/apps/cli/src/commands/vanity-subdomains/activate/activate.handler.ts +++ b/apps/cli/src/commands/vanity-subdomains/activate/activate.handler.ts @@ -83,6 +83,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 6cb5be70e8..282be59786 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 @@ -82,6 +82,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/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/src/shared/init/project-init.templates.ts b/apps/cli/src/shared/init/project-init.templates.ts index 4f4ea35239..a8ef7a97e7 100644 --- a/apps/cli/src/shared/init/project-init.templates.ts +++ b/apps/cli/src/shared/init/project-init.templates.ts @@ -16,7 +16,7 @@ port = 54321 schemas = ["public", "graphql_public"] # Extra schemas to add to the search_path of every request. extra_search_path = ["public", "extensions"] -# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# The maximum number of rows returned from a view, table, or stored procedure. Limits payload size # for accidental or malicious requests. max_rows = 1000 # Controls whether new tables, views, sequences and functions created in the \`public\` schema by @@ -339,11 +339,15 @@ skip_nonce_check = false # If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. email_optional = false -# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS) standard. # You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. [auth.web3.solana] enabled = false +# Allow Ethereum wallet holders to sign in to your project via the Sign in with Ethereum (SIWE, EIP-4361) standard. +# [auth.web3.ethereum] +# enabled = true + # Use Firebase Auth as a third-party provider alongside Supabase Auth. [auth.third_party.firebase] enabled = false diff --git a/apps/cli/src/shared/init/project-init.templates.unit.test.ts b/apps/cli/src/shared/init/project-init.templates.unit.test.ts index 2ce7dda6f6..4bd094b0e4 100644 --- a/apps/cli/src/shared/init/project-init.templates.unit.test.ts +++ b/apps/cli/src/shared/init/project-init.templates.unit.test.ts @@ -65,6 +65,14 @@ const NATIVE_AUTO_EXPOSE_COMMENT = `# without explicit GRANTs, matching the clou # instead. Left unset, a fresh project falls back to \`true\`. # auto_expose_new_tables = true`; +/* Only the native template documents Ethereum web3, cites SIWS without EIP-4361 and fixes + the `max_rows` comment typo; the vendored scaffold is left as shipped. */ +const NATIVE_WEB3_ETHEREUM_EXAMPLE = ` +# Allow Ethereum wallet holders to sign in to your project via the Sign in with Ethereum (SIWE, EIP-4361) standard. +# [auth.web3.ethereum] +# enabled = true +`; + const renderExpectedNativeEject = renderExpectedInitOutput.pipe( Effect.map((eject) => eject @@ -72,7 +80,13 @@ const renderExpectedNativeEject = renderExpectedInitOutput.pipe( '# content_path = "./templates/password_changed_notification.html"', '# content_path = "./supabase/templates/password_changed_notification.html"', ) - .replace(SCAFFOLD_AUTO_EXPOSE_COMMENT, NATIVE_AUTO_EXPOSE_COMMENT), + .replace(SCAFFOLD_AUTO_EXPOSE_COMMENT, NATIVE_AUTO_EXPOSE_COMMENT) + .replace("rows returns from", "rows returned from") + .replace("(SIWS, EIP-4361)", "(SIWS)") + .replace( + "[auth.web3.solana]\nenabled = false\n", + `[auth.web3.solana]\nenabled = false\n${NATIVE_WEB3_ETHEREUM_EXAMPLE}`, + ), ), ); diff --git a/apps/cli/src/shared/output/normalize-error.ts b/apps/cli/src/shared/output/normalize-error.ts index 09f36f5b94..0de304ffd2 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. `suggestUseApiRetry`'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 e146145d56..a848a05a76 100644 --- a/apps/cli/src/shared/output/output.layer.ts +++ b/apps/cli/src/shared/output/output.layer.ts @@ -31,6 +31,7 @@ import { Tty } from "../runtime/tty.service.ts"; import { CommandTarget } from "./command-target.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"; @@ -514,7 +515,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) { @@ -612,7 +619,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 @@ -705,7 +718,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. diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 0446e47bdf..e16164a315 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -5,9 +5,9 @@ # 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 +FROM supabase/postgres:15.19.0.008 AS pg15 # Postgres 14 has no slim build; bumped by hand (Dependabot ignores supabase/postgres). FROM supabase/postgres:14.1.0.89 AS pg14 # New always-on service alias: extend SERVICE_IMAGE_ALIASES in @@ -21,11 +21,11 @@ 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 -FROM supabase/edge-runtime:v1.77.8 AS edgeruntime +FROM supabase/edge-runtime:v1.77.9 AS edgeruntime FROM timberio/vector:0.58.0-alpine AS vector @@ -33,9 +33,9 @@ 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.149.1 AS realtime -FROM supabase/storage-api:v1.84.21 AS storage +FROM supabase/storage-api:v1.84.33 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/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/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 23d6fb0546..3a1f84e957 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)); @@ -231,14 +235,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: @@ -248,16 +252,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); @@ -277,12 +279,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"); @@ -315,7 +317,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"); }), ); @@ -324,9 +326,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)); @@ -340,7 +343,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"); @@ -357,10 +360,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 }), ); @@ -380,7 +383,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 475020b020..04d28ed12a 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/shared/config/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"; @@ -80,6 +84,13 @@ import { mockTty, processEnvLayer, } from "./mocks.ts"; +import { EXEC_FAILED_STATE } from "./docker-health.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. @@ -185,11 +196,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; }), @@ -425,7 +434,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", @@ -559,11 +568,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 = { @@ -577,7 +582,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); @@ -666,7 +671,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}`); @@ -697,7 +702,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); }); @@ -743,15 +748,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) { @@ -764,6 +784,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 — including a surrounding @@ -776,20 +804,14 @@ 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)), () => value === undefined ? withoutConfigEnvPin(name, body) : withConfigEnv({ [name]: value }, body), (previous) => Effect.sync(() => { - if (previous === undefined) delete process.env[name]; - else process.env[name] = previous; + setEnvVar(process.env, name, previous); }), ); @@ -939,27 +961,15 @@ export const FAKE_SHADOW_CONTAINER_ID = "abc123456789shadow0".padEnd(64, "0").sl /** Inspect JSON for a healthy container. */ const SHADOW_HEALTHY_STATE = '{"Running":true,"Status":"running","Health":{"Status":"healthy"}}'; -/** - * A real (Docker-valid) "still starting" state, not `Effect.never`, so - * {@link waitForHealthyServices}'s retry loop genuinely retries on its real 1-second - * `Schedule.spaced` backoff instead of hanging on a single probe forever — a fiber interrupted - * mid-retry must be observed actually suspended inside the retry loop, not merely past the - * initial `create` call. - */ -const SHADOW_STARTING_STATE = '{"Running":true,"Status":"running","Health":{"Status":"starting"}}'; - /** * Fakes every `docker`/`podman` subprocess call the native shadow-provisioning path issues: * `buildLocalDbContainerInputs`'s image-cache check, `createShadowDatabase`'s network-create + - * container create/start, `waitForHealthyServices`'s container inspect, and + * container create/start, `waitForPostgresReady`'s container inspect, and * `removeShadowDatabase`'s cleanup. Both callers only ever create one (shadow) container, never * named. * - * `neverHealthy` (default `false`) makes every `container inspect` report `"starting"` instead of - * `"healthy"`, for interrupt-during-health-wait coverage: with the default healthy-immediately - * response, a forked fiber can run the entire shadow-provisioning sequence to completion - * synchronously before a test's own polling loop is even scheduled, making `Fiber.interrupt` a - * no-op on an already-finished fiber. + * `execFailedHealth` (default `false`) makes every `container inspect` report the health state of a + * host where Docker cannot `docker exec` a HEALTHCHECK (`EXEC_FAILED_STATE`) instead of `"healthy"`. * * `failCreate`/`failRemove` (both default `false`) make `docker create`/`docker rm` exit * non-zero instead. @@ -968,7 +978,7 @@ const SHADOW_STARTING_STATE = '{"Running":true,"Status":"running","Health":{"Sta * supabase_db_` probe `isLocalDbRunning` issues before `--use-pgadmin` provisions * anything — distinguished from the shadow's own `container inspect <64-hex-id>` health probe by * the target id's `supabase_db_` prefix, so both options leave the shadow's own health check on - * its normal (healthy/never-healthy) path. `dbNotRunning` reports Docker's "container doesn't + * its normal path. `dbNotRunning` reports Docker's "container doesn't * exist" shape (`isContainerNotFoundMessage`); mutually exclusive with `dbInspectFailsWith`, * which instead reports a daemon-unreachable failure (`isDockerDaemonUnreachable`) with the given * stderr text — enforced below (a test that sets both throws immediately, rather than one option @@ -980,7 +990,7 @@ const SHADOW_STARTING_STATE = '{"Running":true,"Status":"running","Health":{"Sta */ export function mockShadowContainerCliSpawner( opts: { - readonly neverHealthy?: boolean; + readonly execFailedHealth?: boolean; readonly failCreate?: boolean; readonly failRemove?: boolean; readonly dbNotRunning?: boolean; @@ -996,7 +1006,7 @@ export function mockShadowContainerCliSpawner( "mockShadowContainerCliSpawner: dbNotRunning and dbInspectFailsWith are mutually exclusive", ); } - const neverHealthy = opts.neverHealthy ?? false; + const execFailedHealth = opts.execFailedHealth ?? false; const failCreate = opts.failCreate ?? false; const failRemove = opts.failRemove ?? false; const spawned: Array<{ readonly args: ReadonlyArray }> = []; @@ -1009,15 +1019,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 = @@ -1045,7 +1053,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)]), @@ -1071,7 +1079,7 @@ export function mockShadowContainerCliSpawner( stdoutLines = [FAKE_SHADOW_CONTAINER_ID]; } } else if (args[0] === "container" && args[1] === "inspect") { - stdoutLines = [neverHealthy ? SHADOW_STARTING_STATE : SHADOW_HEALTHY_STATE]; + stdoutLines = [execFailedHealth ? EXEC_FAILED_STATE : SHADOW_HEALTHY_STATE]; } else if (args[0] === "rm") { if (failRemove) { exitCode = 1; @@ -1361,7 +1369,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-health.ts b/apps/cli/tests/helpers/docker-health.ts new file mode 100644 index 0000000000..41f97abbd0 --- /dev/null +++ b/apps/cli/tests/helpers/docker-health.ts @@ -0,0 +1,48 @@ +/** What Docker records for a HEALTHCHECK probe it could not start on a host where `docker exec` fails. */ +export const EXEC_FAILURE_OUTPUT = + "OCI runtime exec failed: exec failed: unable to start container process: error executing setns process: exit status 1: unknown"; + +/** Moby records a probe that outlived its timeout with the same exit code as an exec-start failure. */ +export const PROBE_TIMEOUT_OUTPUT = "Health check exceeded timeout (2s)"; + +export interface RecordedProbe { + readonly exitCode: number; + readonly output: string; +} + +/** The `docker container inspect --format '{{json .State}}'` shape, including `Health.Log`. */ +export function inspectedState( + opts: { + readonly health?: string; + readonly probes?: ReadonlyArray; + readonly status?: string; + readonly running?: boolean; + } = {}, +): string { + return JSON.stringify({ + Status: opts.status ?? "running", + Running: opts.running ?? true, + ExitCode: 0, + OOMKilled: false, + ...(opts.health === undefined + ? {} + : { + Health: { + Status: opts.health, + FailingStreak: opts.health === "healthy" ? 0 : (opts.probes?.length ?? 0), + Log: (opts.probes ?? []).map((probe) => ({ + Start: "2026-10-27T10:00:00.000000000Z", + End: "2026-10-27T10:00:00.100000000Z", + ExitCode: probe.exitCode, + Output: probe.output, + })), + }, + }), + }); +} + +/** A running container whose HEALTHCHECK has never been able to start. */ +export const EXEC_FAILED_STATE = inspectedState({ + health: "unhealthy", + probes: [{ exitCode: -1, output: EXEC_FAILURE_OUTPUT }], +}); 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 f41a504057..eca7e09a6d 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 = ["--remote", "--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,54 @@ 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", - `--remote=${project.dbUrl}`, - "--yes", - ]); - requireCliSuccess(reverted, `migration repair cleanup for ${version}`); + return runLive( + Effect.gen(function* () { + const reverted = yield* awaitForeign(() => + cli([ + "migration", + "repair", + version, + "--status", + "reverted", + `--remote=${project.dbUrl}`, + "--yes", + ]), + ); + yield* callForeign(() => + requireCliSuccess(reverted, `migration repair cleanup for ${version}`), + ); + }), + ); } /** @@ -229,29 +331,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 +370,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 +386,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 572845a027..f391dd4352 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 @@ -62,8 +68,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 ea08640355..eec45e65cc 100644 --- a/apps/cli/tests/helpers/mocks.ts +++ b/apps/cli/tests/helpers/mocks.ts @@ -1,9 +1,20 @@ import { tmpdir } from "node:os"; -import { join } from "node:path"; import process from "node:process"; -import { BunServices } from "@effect/platform-bun"; +import { BunPath, BunServices } from "@effect/platform-bun"; import { ConfigEnvPins, definedEnv } from "./config-env-pins.ts"; -import { Console, ConfigProvider, Deferred, Effect, Layer, Option, Stream } from "effect"; +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"; @@ -52,16 +63,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 = @@ -287,25 +315,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()), }); } }), @@ -535,7 +563,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", @@ -550,34 +578,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 { const providerLayer = 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, @@ -585,7 +622,7 @@ export function processEnvLayer( }), ({ snapshot }) => Effect.sync(() => { - applyProcessEnv(snapshot); + applyProcessEnv(process.env, snapshot); }), ).pipe(Effect.map(({ provider }) => provider)), ); @@ -666,10 +703,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 46c422b227..c6b2812380 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 { @@ -427,15 +424,28 @@ export function storageConsentServices(workdir: string, routes: ReadonlyArray { 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 c67e1595bb..add722bd3d 100644 --- a/apps/cli/tests/smoke-test-macos.ts +++ b/apps/cli/tests/smoke-test-macos.ts @@ -1,12 +1,12 @@ +import { BunFileSystem, BunPath } from "@effect/platform-bun"; import { $ } from "bun"; -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: { @@ -17,136 +17,232 @@ 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)), + ); } -} - -{ - const arch = process.arch; - const binDir = path.join(root, "packages", `cli-darwin-${arch}`, "bin"); - const binaries = ["supabase"]; - - 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" }); - } + if ( + (Predicate.isTagged(failure, "ReleaseStepError") || + Predicate.isTagged(failure, "PlatformError")) && + Predicate.hasProperty(failure, "cause") + ) { + return failure.cause; } + return failure; } -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, -); +/** `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, "../../.."); -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); + interface TestResult { + name: string; + status: "pass" | "fail"; + } - // 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" }); + }), + ), + ); + } - 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(); + { + const arch = process.arch; + const binDir = path.join(root, "packages", `cli-darwin-${arch}`, "bin"); + const binaries = ["supabase"]; + + 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)); + + 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.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), + ), + ); }, }; } 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/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index ee59febc55..749461523f 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,103 +259,220 @@ "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", - "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", "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 @@ -252,18 +481,35 @@ "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", - "description": "Secret key for the CAPTCHA provider." + "description": "Secret key for the CAPTCHA provider.", + "x-secret": true } }, "additionalProperties": false @@ -275,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", @@ -285,7 +539,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 @@ -294,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", @@ -304,7 +567,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 @@ -313,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", @@ -323,7 +595,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 @@ -332,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", @@ -342,7 +623,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 @@ -351,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", @@ -361,7 +651,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 @@ -370,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", @@ -380,7 +679,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 @@ -395,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 @@ -411,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", @@ -442,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 @@ -481,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", @@ -506,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", @@ -537,7 +965,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", @@ -595,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", @@ -632,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", @@ -652,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", @@ -686,7 +1163,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 @@ -695,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", @@ -709,7 +1195,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 @@ -718,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", @@ -728,7 +1223,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 @@ -737,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", @@ -747,7 +1251,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 @@ -756,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", @@ -770,7 +1283,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 @@ -792,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", @@ -804,7 +1326,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", @@ -817,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 @@ -833,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", @@ -845,7 +1392,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", @@ -858,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 @@ -874,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", @@ -886,7 +1458,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", @@ -899,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 @@ -915,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", @@ -927,7 +1524,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", @@ -940,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 @@ -956,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", @@ -968,7 +1590,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", @@ -981,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 @@ -997,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", @@ -1009,7 +1656,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", @@ -1022,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 @@ -1038,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", @@ -1050,7 +1722,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", @@ -1063,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 @@ -1079,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", @@ -1091,7 +1788,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", @@ -1104,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 @@ -1120,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", @@ -1132,7 +1854,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", @@ -1145,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 @@ -1161,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", @@ -1173,7 +1920,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", @@ -1186,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 @@ -1202,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", @@ -1214,7 +1986,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", @@ -1228,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 @@ -1244,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", @@ -1256,7 +2053,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", @@ -1269,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 @@ -1285,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", @@ -1297,7 +2119,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", @@ -1310,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 @@ -1326,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", @@ -1338,7 +2185,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", @@ -1351,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 @@ -1367,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", @@ -1379,7 +2251,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", @@ -1392,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 @@ -1408,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", @@ -1420,7 +2317,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", @@ -1433,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 @@ -1449,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", @@ -1461,7 +2383,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", @@ -1474,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 @@ -1490,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", @@ -1502,7 +2449,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", @@ -1515,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 @@ -1531,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", @@ -1543,7 +2515,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", @@ -1556,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 @@ -1572,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", @@ -1584,7 +2581,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", @@ -1597,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 @@ -1619,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 @@ -1630,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 @@ -1644,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", @@ -1654,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 @@ -1668,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", @@ -1683,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", @@ -1702,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", @@ -1721,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", @@ -1736,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", @@ -1757,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", @@ -1772,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", @@ -1784,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 @@ -1816,150 +2966,310 @@ "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\\((.*)\\)$" + } + ] }, "checks": { "type": "object", "properties": { "rls_disabled_in_public": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for rls_disabled_in_public: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_enabled_no_policy": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for rls_enabled_no_policy: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "policy_exists_rls_disabled": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for policy_exists_rls_disabled: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "security_definer_view": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for security_definer_view: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_policy_always_true": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for rls_policy_always_true: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sensitive_columns_exposed": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for sensitive_columns_exposed: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "auth_users_exposed": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for auth_users_exposed: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "function_search_path_mutable": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for function_search_path_mutable: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_references_user_metadata": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for rls_references_user_metadata: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "exclude": { "type": "object", "properties": { "rls_disabled_in_public": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects rls_disabled_in_public skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects rls_disabled_in_public skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_enabled_no_policy": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects rls_enabled_no_policy skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects rls_enabled_no_policy skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "policy_exists_rls_disabled": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects policy_exists_rls_disabled skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects policy_exists_rls_disabled skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "security_definer_view": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects security_definer_view skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects security_definer_view skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_policy_always_true": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects rls_policy_always_true skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects rls_policy_always_true skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sensitive_columns_exposed": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects sensitive_columns_exposed skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects sensitive_columns_exposed skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "auth_users_exposed": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects auth_users_exposed skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects auth_users_exposed skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "function_search_path_mutable": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects function_search_path_mutable skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects function_search_path_mutable skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_references_user_metadata": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects rls_references_user_metadata skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects rls_references_user_metadata skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false, @@ -1976,18 +3286,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 @@ -2005,22 +3331,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" @@ -2035,15 +3409,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" @@ -2055,7 +3453,15 @@ "type": "string" }, "track_commit_timestamp": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "wal_keep_size": { "type": "string" @@ -2073,25 +3479,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 @@ -2100,9 +3530,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 @@ -2111,7 +3549,8 @@ "type": "object", "additionalProperties": { "type": "string", - "description": "Vault secret value." + "description": "Vault secret value.", + "x-secret": true }, "description": "Vault secrets." } @@ -2122,31 +3561,64 @@ "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", "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." } @@ -2164,14 +3636,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", @@ -2184,13 +3672,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", @@ -2225,22 +3721,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", @@ -2257,20 +3785,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 @@ -2279,9 +3831,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": [ @@ -2290,6 +3850,10 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, @@ -2297,9 +3861,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable image transformation.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2312,9 +3884,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": [ @@ -2323,17 +3903,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", @@ -2354,9 +3946,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 @@ -2365,24 +3965,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": [ @@ -2420,19 +4052,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": [ @@ -2473,14 +4129,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", @@ -2490,7 +4162,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 @@ -2519,10 +4192,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", @@ -2530,12 +4211,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 @@ -2553,12 +4242,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", @@ -2578,20 +4283,30 @@ "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", "properties": { "enabled": { - "type": "boolean", "description": "Enable experimental webhooks.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2600,9 +4315,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use pg-delta as the schema diff engine for db diff / db pull (the default). Set false to fall back to the legacy migra engine.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "declarative_schema_path": { "type": "string", @@ -2843,24 +4566,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", @@ -2881,47 +4636,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", @@ -2945,9 +4756,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", @@ -2955,18 +4774,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", @@ -2977,103 +4812,220 @@ "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", - "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", "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 @@ -3082,18 +5034,35 @@ "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", - "description": "Secret key for the CAPTCHA provider." + "description": "Secret key for the CAPTCHA provider.", + "x-secret": true } }, "additionalProperties": false @@ -3105,9 +5074,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", @@ -3115,7 +5092,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 @@ -3124,9 +5102,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", @@ -3134,7 +5120,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 @@ -3143,9 +5130,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", @@ -3153,7 +5148,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 @@ -3162,9 +5158,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", @@ -3172,7 +5176,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 @@ -3181,9 +5186,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", @@ -3191,7 +5204,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 @@ -3200,9 +5214,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", @@ -3210,7 +5232,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 @@ -3225,14 +5248,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 @@ -3241,19 +5280,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", @@ -3272,22 +5335,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 @@ -3311,24 +5398,56 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via email to your project.", - "default": true - }, - "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\\((.*)\\)$" + } + ] + }, + "double_confirm_changes": { + "description": "If enabled, a user will be required to confirm any email change on both the old and new email addresses.", + "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", @@ -3336,30 +5455,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", @@ -3367,7 +5518,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", @@ -3425,9 +5577,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", @@ -3462,14 +5622,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", @@ -3482,22 +5658,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", @@ -3516,7 +5716,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 @@ -3525,9 +5726,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", @@ -3539,7 +5748,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 @@ -3548,9 +5758,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", @@ -3558,7 +5776,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 @@ -3567,9 +5786,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", @@ -3577,7 +5804,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 @@ -3586,9 +5814,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", @@ -3600,7 +5836,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 @@ -3622,9 +5859,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", @@ -3634,7 +5879,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", @@ -3647,14 +5893,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 @@ -3663,9 +5925,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", @@ -3675,7 +5945,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", @@ -3688,14 +5959,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 @@ -3704,9 +5991,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", @@ -3716,7 +6011,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", @@ -3729,14 +6025,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 @@ -3745,9 +6057,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", @@ -3757,7 +6077,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", @@ -3770,14 +6091,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 @@ -3786,9 +6123,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", @@ -3798,7 +6143,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", @@ -3811,14 +6157,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 @@ -3827,9 +6189,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", @@ -3839,7 +6209,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", @@ -3852,14 +6223,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 @@ -3868,9 +6255,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", @@ -3880,7 +6275,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", @@ -3893,14 +6289,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 @@ -3909,9 +6321,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", @@ -3921,7 +6341,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", @@ -3934,14 +6355,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 @@ -3950,9 +6387,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", @@ -3962,7 +6407,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", @@ -3975,14 +6421,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 @@ -3991,9 +6453,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", @@ -4003,7 +6473,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", @@ -4016,14 +6487,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 @@ -4032,9 +6519,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", @@ -4044,7 +6539,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", @@ -4058,14 +6554,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 @@ -4074,9 +6586,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", @@ -4086,7 +6606,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", @@ -4099,14 +6620,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 @@ -4115,9 +6652,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", @@ -4127,7 +6672,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", @@ -4140,14 +6686,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 @@ -4156,9 +6718,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", @@ -4168,7 +6738,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", @@ -4181,14 +6752,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 @@ -4197,9 +6784,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", @@ -4209,7 +6804,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", @@ -4222,14 +6818,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 @@ -4238,9 +6850,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", @@ -4250,7 +6870,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", @@ -4263,14 +6884,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 @@ -4279,9 +6916,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", @@ -4291,7 +6936,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", @@ -4304,14 +6950,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 @@ -4320,9 +6982,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", @@ -4332,7 +7002,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", @@ -4345,25 +7016,49 @@ "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 }, "workos": { "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", @@ -4373,7 +7068,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", @@ -4386,14 +7082,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 @@ -4402,9 +7114,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", @@ -4414,7 +7134,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", @@ -4427,14 +7148,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 @@ -4449,9 +7186,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 @@ -4460,9 +7205,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 @@ -4474,9 +7227,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", @@ -4484,9 +7245,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 @@ -4498,9 +7267,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", @@ -4513,9 +7290,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", @@ -4532,9 +7317,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", @@ -4551,9 +7344,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", @@ -4566,9 +7367,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", @@ -4587,14 +7396,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", @@ -4602,9 +7427,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", @@ -4614,30 +7447,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 @@ -4646,150 +7519,310 @@ "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\\((.*)\\)$" + } + ] }, "checks": { "type": "object", "properties": { "rls_disabled_in_public": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for rls_disabled_in_public: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_enabled_no_policy": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for rls_enabled_no_policy: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "policy_exists_rls_disabled": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for policy_exists_rls_disabled: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "security_definer_view": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for security_definer_view: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_policy_always_true": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for rls_policy_always_true: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sensitive_columns_exposed": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for sensitive_columns_exposed: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "auth_users_exposed": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for auth_users_exposed: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "function_search_path_mutable": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for function_search_path_mutable: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_references_user_metadata": { - "type": "string", - "enum": ["error", "warn", "off"], "description": "Severity for rls_references_user_metadata: \"error\" will block the migration; until blocking ships it reports like \"warn\". \"warn\" reports it, \"off\" skips the rule.", - "default": "warn" + "default": "warn", + "anyOf": [ + { + "type": "string", + "enum": ["error", "warn", "off"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "exclude": { "type": "object", "properties": { "rls_disabled_in_public": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects rls_disabled_in_public skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects rls_disabled_in_public skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_enabled_no_policy": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects rls_enabled_no_policy skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects rls_enabled_no_policy skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "policy_exists_rls_disabled": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects policy_exists_rls_disabled skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects policy_exists_rls_disabled skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "security_definer_view": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects security_definer_view skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects security_definer_view skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_policy_always_true": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects rls_policy_always_true skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects rls_policy_always_true skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sensitive_columns_exposed": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects sensitive_columns_exposed skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects sensitive_columns_exposed skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "auth_users_exposed": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects auth_users_exposed skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects auth_users_exposed skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "function_search_path_mutable": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects function_search_path_mutable skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects function_search_path_mutable skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "rls_references_user_metadata": { - "type": "array", - "items": { - "type": "string", - "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" - }, - "description": "Objects rls_references_user_metadata skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function." + "description": "Objects rls_references_user_metadata skips, each written \"schema.name\", for example \"public.countries\". An entry like \"public.set_updated_at\" matches every overload of that function.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(?!\\s)[^.]*[^.\\s]\\.(?!\\s)[\\s\\S]*\\S(?![\\s\\S])" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false, @@ -4806,18 +7839,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 @@ -4835,22 +7884,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" @@ -4865,15 +7962,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" @@ -4885,7 +8006,15 @@ "type": "string" }, "track_commit_timestamp": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "wal_keep_size": { "type": "string" @@ -4903,25 +8032,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 @@ -4930,9 +8083,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 @@ -4941,7 +8102,8 @@ "type": "object", "additionalProperties": { "type": "string", - "description": "Vault secret value." + "description": "Vault secret value.", + "x-secret": true }, "description": "Vault secrets." } @@ -4952,31 +8114,64 @@ "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", "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." } @@ -4994,14 +8189,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", @@ -5014,13 +8225,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", @@ -5055,22 +8274,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", @@ -5087,20 +8338,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 @@ -5109,9 +8384,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": [ @@ -5120,6 +8403,10 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, @@ -5127,9 +8414,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable image transformation.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -5142,9 +8437,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": [ @@ -5153,17 +8456,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", @@ -5184,9 +8499,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 @@ -5195,24 +8518,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": [ @@ -5250,19 +8605,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": [ @@ -5303,14 +8682,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", @@ -5320,7 +8715,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 @@ -5349,10 +8745,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", @@ -5360,12 +8764,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 @@ -5383,12 +8795,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", @@ -5408,20 +8836,30 @@ "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", "properties": { "enabled": { - "type": "boolean", "description": "Enable experimental webhooks.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -5430,9 +8868,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use pg-delta as the schema diff engine for db diff / db pull (the default). Set false to fall back to the legacy migra engine.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "declarative_schema_path": { "type": "string", diff --git a/package.json b/package.json index 4edb7c3061..e0b88a787c 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/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, + ), ); }); 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`. */ 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 b51a6468f3..d9d63ab128 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(), @@ -144,37 +202,53 @@ export async function exec( // The CLI resolves its API base URL through the profile system, not // SUPABASE_API_URL: `CommandSettings` accepts a built-in profile name or a YAML // file path. Write a temp profile file pointing at the replay server. - 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/packages/config/AGENTS.md b/packages/config/AGENTS.md index fcbe93190a..8452788859 100644 --- a/packages/config/AGENTS.md +++ b/packages/config/AGENTS.md @@ -55,7 +55,7 @@ artifacts (`./schema.json`, `./project-schema.json`). Migration-check vocabulary for the CLI resolver: `MIGRATION_CHECK_RULES`, `mapMigrationCheckRules`, `DEFAULT_MIGRATION_CHECK_SEVERITY`, `MigrationCheckRule`, and `MigrationCheckSeverity`. - `@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 461930b8ef..6f6af2e211 100644 --- a/packages/config/src/base.ts +++ b/packages/config/src/base.ts @@ -83,9 +83,10 @@ export const CliConfigSchema = Schema.Struct({ }); export function toCliConfigJsonSchema() { - return toPublishedJsonSchemaDocument( - Schema.toJsonSchemaDocument(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 730f9d55b0..4b31c8e65a 100644 --- a/packages/config/src/json-schema-document.ts +++ b/packages/config/src/json-schema-document.ts @@ -1,19 +1,39 @@ +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"]); + /** - * 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. A bare object whose only constraint is one object - * schema in `allOf` is replaced by that schema, so editors read its properties directly. + * 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. A bare object whose only constraint is + * one object schema in `allOf` is replaced by that schema, so editors read its properties directly. */ -export function toPublishedJsonSchemaDocument(document: { - readonly schema: Record; - readonly definitions: Record; -}) { +export function toPublishedJsonSchemaDocument( + schema: Schema.Constraint, + 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 } : {}), }; } @@ -51,3 +71,71 @@ 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) && !isEmptyLiteral(node); + return node["type"] === "array" && isFreeFormString(node["items"]); +} + +function isEmptyLiteral(node: Node): boolean { + const values = node["enum"]; + return Array.isArray(values) && values.length === 1 && values[0] === ""; +} + +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 81674ed5ed..af190f0d16 100644 --- a/packages/config/src/json-schema-document.unit.test.ts +++ b/packages/config/src/json-schema-document.unit.test.ts @@ -1,5 +1,16 @@ +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_PATTERN, + env, + interpolateEnvReferencesAgainstSchema, + isEnvReference, +} from "./lib/env.ts"; +import { secretPathPatterns } from "./lib/secret-paths.ts"; import { toProjectConfigJsonSchema } from "./project-config/project-schema.ts"; type Node = Record; @@ -10,6 +21,190 @@ 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 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, + { + cliCompat: 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 => { + 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(); @@ -50,11 +245,11 @@ describe("published JSON Schema documents", () => { expect(Object.keys(at(exclude, "properties")).sort()).toEqual([...rules].sort()); for (const rule of rules) { expect(at(checks, "properties", rule)).toMatchObject({ - type: "string", - enum: ["error", "warn", "off"], + anyOf: expect.arrayContaining([{ type: "string", enum: ["error", "warn", "off"] }]), }); - expect(at(exclude, "properties", rule)).toMatchObject({ type: "array" }); - expect(at(exclude, "properties", rule, "items").pattern).toEqual(expect.any(String)); + const list = at(exclude, "properties", rule, "anyOf", 0); + expect(list).toMatchObject({ type: "array" }); + expect(at(list, "items").pattern).toEqual(expect.any(String)); } }); @@ -73,4 +268,419 @@ 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"); + }); +}); + +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/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" }); } diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index b40b1aa6d4..7bd75f56ae 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", }, }, }, @@ -157,24 +157,24 @@ const definitions: Readonly> = { "bin/psql", ], { - "15.19.0.007": { - upstreamVersion: "15.19.0.007", + "15.19.0.008": { + upstreamVersion: "15.19.0.008", revision: 0, image: - "ghcr.io/supabase/cli/postgres:15.19.0.007-r0@sha256:7698932a5a3878d8eba9b5cc8ce75ff29162744b7878b5892b33849c310437cf", - upstreamImage: "supabase/postgres:15.19.0.007", + "ghcr.io/supabase/cli/postgres:15.19.0.008-r0@sha256:a3d0932e880417c1c729c9344febd787d6e5b810e53a563cb8752b6467cf8fdf", + upstreamImage: "supabase/postgres:15.19.0.008", natives: { "darwin-arm64": { - archive: "3c35ed10ac30ccd645eb684777d723bc7855d4fc5a39bacd7100337679a30a4d", - manifest: "a1f11b1d68c83d4905c48ad9851eaef7a68584f4dd3df1d520f107d5e23c3595", + archive: "32bb35483a40d11797fab1d17fd454d01bbeaf1c235d18547890766f73003a1b", + manifest: "4c4d840eda2beaaf5ae0f0607ae34243b949762a0f32bd1a32aef3db16acac73", }, "linux-amd64": { - archive: "61f1f74855e52bcabe95db07c70ac54a086b5fbd113c007810a0bee2ad072051", - manifest: "64c70c66bc3b71324727a059921074e0722284a4cbb077914d71a22d513b4e55", + archive: "1b116cf465d4331b982e814c78e3c871bcd8de834540bdf1be263280e1721402", + manifest: "226724e98aff3f0ceffc9af0b0906f030612244cd07da41ba71c46dd2295f87c", }, "linux-arm64": { - archive: "5b45fb61144b04154f3568dcedae941e765b6da32b7a1ea7d0c5ae6becadc048", - manifest: "fed2eac98a385378a3c9c4e94dd03d05d2f5acff4900e2071649f98164807aed", + archive: "dc10516c0969357f8c877b981063696c493f701df26a32ac12dbce921ec3da7f", + manifest: "2cc1196fc4c31b79fe2ca2529ba6b42794e7440c30f9213aa9b31129a02afb7c", }, }, }, @@ -317,23 +317,23 @@ const definitions: Readonly> = { realtime: definition( "realtime", { - upstreamVersion: "v2.147.1", + upstreamVersion: "v2.149.1", 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.149.1-r0@sha256:3a1218ee89626cd88487184c2d87f16268ac2b002e96282800aa0037a042324b", + upstreamImage: "supabase/realtime:v2.149.1", natives: { "darwin-arm64": { - archive: "fd05e59cf29c6e8e0523f60ca0f3e823d045178fa7ebdcba934d116c0889c15a", - manifest: "f85b0ae9b9a10671f892298e68912ed0fc2e6828427e7d039d0651edd5600339", + archive: "245518dd7265a06eea5c99227f26180ec276b31b3294c7fa9dee12db0adb5f2a", + manifest: "42a3722ff12b77bfa07963e075e6bb09cacc699639c03b4010553c91627f02e2", }, "linux-amd64": { - archive: "4f50539770db276c890dd582fef5239a3800a4d074311ec739227738e5f3f576", - manifest: "43651c3933e26580605390cb000fdc36603213e10b2a5fbbeaf519178e6eea9c", + archive: "d42c54a35a9e5b50f29e9a2d53b5fd21c2fe60eed4530dddaae363635bddf9ca", + manifest: "954ad956c5e4b06e5ab4f14ab7dfec0f76dc6aa0174f65a4c54a8eda35098d91", }, "linux-arm64": { - archive: "f04da0eaec820690f24a37da0d2b0787e41ef8c6f0d28935639cf6c87d2d0162", - manifest: "705d7c7321537acbb6cbb1b2b3efef5f19dc9a167c31d960082e0323ab200120", + archive: "532466fb9a9ee72569f0a12edd23d9fe5d5d5b5e5fd274adc567a64884b6b802", + manifest: "8e86a1f6dc1da5927e5ea1ddc0a3a0938be7d5452686d59978225ac780402e6e", }, }, }, @@ -343,23 +343,23 @@ const definitions: Readonly> = { storage: definition( "storage", { - upstreamVersion: "v1.84.21", + upstreamVersion: "v1.84.33", 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.33-r0@sha256:7f43434eca2449d01fb18c20588c443ecac5b091ba321043c849593ab5e70915", + upstreamImage: "supabase/storage-api:v1.84.33", natives: { "darwin-arm64": { - archive: "9242f9311ab336ec1f82e91b15bd62a1b1d1ee6602b5e3337f6b314b5d5487d6", - manifest: "0917e94e57a726b345636a5f606b60132c9102f9d1a1f412d475fc9ac10a546c", + archive: "98e5846d8b98b97161f2af4fca060964b7250b5d0d7b71b560ea08ea8a730309", + manifest: "55625a992aa413be1e30d2bf8825913c812e2edf333b83e860c800f879f8b7b2", }, "linux-amd64": { - archive: "9a57bc5d5027c7a2bf7b995ac88168a590f599b2eca63047499ac7a1f2dfc232", - manifest: "45b4439c92c83377d03bf9e2bf3c28bf9d164329a8100573d52f80108a318806", + archive: "b3c41fd8fc12fd8680ca76aa082f3539ba9cdfc2ccbbaa4651a3a4ef3e2e6af7", + manifest: "51e91a6db77fbf7f9f2c22de8d8c0a35bfb0462de06d131e0d143d04fad88ea3", }, "linux-arm64": { - archive: "2b00c4c8ab62e3ca2508ae237ec3ead78152d5d1dbb68c0fa291977f3ed2e2ab", - manifest: "83aae8b24999bb5647673116a523ed4a5e75e103262b5fc4912e5ba4e0258671", + archive: "34143d6934f6371b09d41b8d1c6974959afc71aaeeb782075c3054dfbd2f860f", + manifest: "8cc3e2ef7498db11b40954e6f69ea0b0ac62eebcfc7dbe1fea75af916684c5fe", }, }, }, @@ -394,23 +394,23 @@ const definitions: Readonly> = { functions: definition( "edge-runtime", { - upstreamVersion: "v1.77.8", + upstreamVersion: "v1.77.9", revision: 0, image: - "ghcr.io/supabase/cli/edge-runtime:v1.77.8-r0@sha256:a162f950a1f5f69e40afcb2a72a2dd924fe14884cf6b9591723ef69ddcaaedb2", - upstreamImage: "supabase/edge-runtime:v1.77.8", + "ghcr.io/supabase/cli/edge-runtime:v1.77.9-r0@sha256:1d2d920e96fe72bdff308f855a30a60a51baf4d9c3b396c16b36eb193daf6f61", + upstreamImage: "supabase/edge-runtime:v1.77.9", natives: { "darwin-arm64": { - archive: "6e114f7817beebac350be6bfbd14d12bd3e2b0b2d6e7a2e94eb11ede68213a60", - manifest: "7ee23c9287653a8239963c347a1887f5c541f69bfb356b3f7576cafbf467d528", + archive: "d2d14cadc53254416c1ecc89dd0575efd4733cb72025ff97495d3a87980acf39", + manifest: "1d4ae4b525a5b6f5729e811664dbfdf60bb9bc453a5e71abf6947253c20563b0", }, "linux-amd64": { - archive: "af349b253f0ad192d2eaba0b584a0e8a96118ce1b39f514389f40c66ea4e7502", - manifest: "c560ecd5c85bda5ba13f4deb1fc7138291af3149da018d8e83593b64e225a5ab", + archive: "d61748740756c192d83b2d5e6960bc20e2eb748fd7aee7620bfd4bf5854013b6", + manifest: "a25c0f4e9bfaa1583314906e1cb0ce06b5ac0a78f349e2db5f08a1ed669ce85e", }, "linux-arm64": { - archive: "1360ec1f775b62cc81f2469ddfc53840034e9b9e73e02c2594be332ad3218dbe", - manifest: "3208b8a22583cae5522b64cac87b49c8210a32bf29dde6b24a9f7900bffa787d", + archive: "5e247316b1965f84d8748de0da92e4c8d2af20d6d5e6dccdf5da35e7e0dfbff4", + manifest: "8e259a40d00950f311082b89517ebc04323d3c8106432c88f64e59f61c5c860e", }, }, }, @@ -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", }, }, }, @@ -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/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}`); } 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/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, 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), + })), + ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9b5b44b77..6e437ab91b 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 @@ -528,6 +534,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:' @@ -582,6 +595,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 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 750420023d..17a0aa6227 100644 --- a/tools/release/local-release.ts +++ b/tools/release/local-release.ts @@ -9,18 +9,27 @@ * Requires `pnpm local-registry` to be running in another terminal. */ +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 = [ @@ -73,106 +82,142 @@ 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); - } -} +}); + +/* 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 }, + ), + ); -async function main() { - const { values } = parseArgs({ - options: { - version: { type: "string" }, - }, +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 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(); 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/2] Compiling CLI binary..."); - const buildResult = await Bun.build({ - entrypoints: [entrypoint], - compile: { target: platform.bunTarget, outfile: bunBinary }, - ...compileOptions, - define: await stackReleaseDefine(), - }); + yield* Console.log("[1/2] 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 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("[2/2] Building Node.js shim..."); - await $`bun build ${shimSrc} --outfile=${shimOut} --target=node`; + yield* Console.log("[2/2] 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 @@ -191,31 +236,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: @@ -225,9 +278,25 @@ Or install globally: npm install -g --registry ${REGISTRY} ${umbrellaName}@${version} supabase --version `); - } finally { - await rm(tmpDir, { recursive: true, force: true }); - } -} + }).pipe(Effect.exit); + yield* fs.remove(tmpDir, { recursive: true, force: true }); + return yield* exit; +}); -await main(); +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), + ), + ), + ), +);