From b5a5bf326ebe650e09d5c238f270467d8d34e277 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 26 Oct 2026 15:32:31 +0000 Subject: [PATCH 1/4] feat(config): accept env() references on number, boolean and enum fields in the config schema The loader resolves env(VAR) on any field and coerces it to the field's type, but the published CLI config schema typed number, integer, boolean and enum fields strictly, so env() references there showed as editor errors. Those fields now accept their usual value or an env() string matching the loader's syntax. The project-config schema is unchanged, since it describes resolved values. Closes CLI-2857 --- apps/docs/public/cli/config.schema.json | 4390 ++++++++++++++--- packages/config/AGENTS.md | 2 +- packages/config/package.json | 1 + .../scripts/build-artifacts.unit.test.ts | 6 +- packages/config/src/base.ts | 5 +- packages/config/src/json-schema-document.ts | 91 +- .../src/json-schema-document.unit.test.ts | 495 +- pnpm-lock.yaml | 3 + 8 files changed, 4245 insertions(+), 748 deletions(-) diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 87a9942059..67a81f8623 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,14 +83,30 @@ "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", @@ -77,21 +125,45 @@ "default": ["public", "extensions"] }, "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 +187,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", @@ -134,9 +214,17 @@ "default": ["https://127.0.0.1:3000"] }, "jwt_expiry": { - "type": "number", "description": "How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).", - "default": 3600 + "default": 3600, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "jwt_issuer": { "type": "string", @@ -147,45 +235,101 @@ "description": "Path to the JWT signing keys file." }, "enable_refresh_token_rotation": { - "type": "boolean", "description": "If disabled, the refresh token will never expire.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "refresh_token_reuse_interval": { - "type": "number", "description": "Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_manual_linking": { - "type": "boolean", "description": "Allow/disallow testing manual linking of accounts.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups to your project.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_anonymous_sign_ins": { - "type": "boolean", "description": "Allow/disallow anonymous sign-ins to your project.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "minimum_password_length": { - "type": "number", "description": "Passwords shorter than this value will be rejected as weak.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "password_requirements": { - "type": "string", - "enum": [ - "", - "letters_digits", - "lower_upper_letters_digits", - "lower_upper_letters_digits_symbols" - ], "description": "Password character requirements.", - "default": "" + "default": "", + "anyOf": [ + { + "type": "string", + "enum": [ + "", + "letters_digits", + "lower_upper_letters_digits", + "lower_upper_letters_digits_symbols" + ] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "publishable_key": { "type": "string", @@ -216,39 +360,95 @@ "type": "object", "properties": { "email_sent": { - "type": "number", "description": "Number of emails that can be sent per hour.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sms_sent": { - "type": "number", "description": "Number of SMS messages that can be sent per hour.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "anonymous_users": { - "type": "number", "description": "Number of anonymous sign-ins that can be made per hour per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "token_refresh": { - "type": "number", "description": "Number of sessions that can be refreshed in a 5 minute interval per IP address.", - "default": 150 + "default": 150, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sign_in_sign_ups": { - "type": "number", "description": "Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "token_verifications": { - "type": "number", "description": "Number of OTP or magic link verifications that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "web3": { - "type": "number", "description": "Number of Web3 logins that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -257,14 +457,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable CAPTCHA verification.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "provider": { - "type": "string", - "enum": ["hcaptcha", "turnstile"], - "description": "CAPTCHA provider to use." + "description": "CAPTCHA provider to use.", + "anyOf": [ + { + "type": "string", + "enum": ["hcaptcha", "turnstile"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secret": { "type": "string", @@ -281,9 +497,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the mfa verification hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -301,9 +525,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the password verification hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -321,9 +553,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the custom access token hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -341,9 +581,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the send sms hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -361,9 +609,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the send email hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -381,9 +637,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the before user created hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -407,14 +671,30 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow TOTP enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow TOTP verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -423,19 +703,43 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow phone enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow phone verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_length": { - "type": "number", "description": "The length of the OTP code.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "template": { "type": "string", @@ -454,28 +758,52 @@ "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 - } - }, - "additionalProperties": false - }, - "sessions": { - "type": "object", + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] + } + }, + "additionalProperties": false + }, + "sessions": { + "type": "object", "properties": { "timebox": { "type": "string", @@ -493,24 +821,56 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via email to your project.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "double_confirm_changes": { - "type": "boolean", "description": "If enabled, a user will be required to confirm any email change on both the old and new email addresses.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_confirmations": { - "type": "boolean", "description": "If enabled, users need to confirm their email address before signing in.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secure_password_change": { - "type": "boolean", "description": "If enabled, users will need to reauthenticate or have logged in recently to change their password.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_frequency": { "type": "string", @@ -518,30 +878,62 @@ "default": "1s" }, "otp_length": { - "type": "number", "description": "Number of characters used in the email OTP.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_expiry": { - "type": "number", "description": "Number of seconds before the email OTP expires.", - "default": 3600 + "default": 3600, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "smtp": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable SMTP for email delivery.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "host": { "type": "string", "description": "Hostname or IP address of the SMTP server." }, "port": { - "type": "number", - "description": "Port number of the SMTP server." + "description": "Port number of the SMTP server.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "user": { "type": "string", @@ -608,9 +1000,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the notification email.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "subject": { "type": "string", @@ -645,14 +1045,30 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via SMS to your project.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_confirmations": { - "type": "boolean", "description": "If enabled, users need to confirm their phone number before signing in.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "template": { "type": "string", @@ -665,22 +1081,46 @@ "default": "5s" }, "otp_length": { - "type": "number", "description": "Number of characters used in the SMS OTP.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_expiry": { - "type": "number", "description": "Number of seconds before the SMS OTP expires.", - "default": 60 + "default": 60, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "twilio": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Twilio provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "account_sid": { "type": "string", @@ -709,9 +1149,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Twilio Verify provider for phone verification.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "account_sid": { "type": "string", @@ -733,9 +1181,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable MessageBird provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "originator": { "type": "string", @@ -753,9 +1209,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Textlocal provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sender": { "type": "string", @@ -773,9 +1237,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Vonage provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "from": { "type": "string", @@ -810,9 +1282,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Apple OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -836,14 +1316,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -852,9 +1348,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Azure OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -878,14 +1382,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -894,9 +1414,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Bitbucket OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -920,14 +1448,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -936,9 +1480,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Discord OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -962,14 +1514,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -978,9 +1546,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Facebook OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1004,14 +1580,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1020,9 +1612,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Figma OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1046,14 +1646,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1062,9 +1678,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the GitHub OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1088,14 +1712,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1104,9 +1744,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the GitLab OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1130,14 +1778,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1146,9 +1810,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Google OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1172,14 +1844,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1188,9 +1876,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Kakao OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1214,14 +1910,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1230,9 +1942,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Keycloak OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1257,14 +1977,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1273,9 +2009,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the LinkedIn OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1299,14 +2043,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1315,9 +2075,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Notion OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1341,14 +2109,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1357,9 +2141,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Twitch OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1383,14 +2175,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1399,9 +2207,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Twitter OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1425,14 +2241,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1441,9 +2273,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the X OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1467,14 +2307,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1483,10 +2339,18 @@ "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", "description": "Client ID for the Slack OAuth provider.", @@ -1509,14 +2373,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1525,9 +2405,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Spotify OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1551,14 +2439,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1567,9 +2471,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the WorkOS OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1593,14 +2505,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1609,9 +2537,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Zoom OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -1635,14 +2571,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1657,9 +2609,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this Web3 provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1668,9 +2628,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this Web3 provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1682,9 +2650,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable OAuth server functionality.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "authorization_url_path": { "type": "string", @@ -1692,9 +2668,17 @@ "default": "/oauth/consent" }, "allow_dynamic_registration": { - "type": "boolean", "description": "Allow dynamic client registration.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1706,9 +2690,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "project_id": { "type": "string", @@ -1721,9 +2713,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "tenant": { "type": "string", @@ -1740,9 +2740,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "user_pool_id": { "type": "string", @@ -1759,9 +2767,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "domain": { "type": "string", @@ -1774,9 +2790,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "issuer_url": { "type": "string", @@ -1795,14 +2819,30 @@ "type": "object", "properties": { "port": { - "type": "number", "description": "Port to use for the local database URL.", - "default": 54322 + "default": 54322, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "shadow_port": { - "type": "number", "description": "Port used by db diff command to initialize the shadow database.", - "default": 54320 + "default": 54320, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "health_timeout": { "type": "string", @@ -1810,9 +2850,17 @@ "default": "2m" }, "major_version": { - "type": "number", "description": "The database major version to use. This has to be the same as your remote database's.", - "default": 17 + "default": 17, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "orioledb_version": { "type": "string", @@ -1822,30 +2870,70 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local PgBouncer service.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the local connection pooler.", - "default": 54329 + "default": 54329, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "pool_mode": { - "type": "string", - "enum": ["transaction", "session"], "description": "Specifies when a server connection can be reused by other clients.", - "default": "transaction" + "default": "transaction", + "anyOf": [ + { + "type": "string", + "enum": ["transaction", "session"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "default_pool_size": { - "type": "number", "description": "How many server connections to allow per user/database pair.", - "default": 20 + "default": 20, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_client_conn": { - "type": "number", "description": "Maximum number of client connections allowed.", - "default": 100 + "default": 100, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -1854,9 +2942,17 @@ "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", @@ -1874,9 +2970,17 @@ "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", @@ -1903,22 +3007,70 @@ "type": "string" }, "max_connections": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_locks_per_transaction": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_maintenance_workers": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_workers": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_workers_per_gather": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_replication_slots": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_slot_wal_keep_size": { "type": "string" @@ -1933,15 +3085,39 @@ "type": "string" }, "max_wal_senders": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_worker_processes": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "session_replication_role": { - "type": "string", - "enum": ["origin", "replica", "local"], - "description": "Session replication role." + "description": "Session replication role.", + "anyOf": [ + { + "type": "string", + "enum": ["origin", "replica", "local"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "shared_buffers": { "type": "string" @@ -1953,7 +3129,15 @@ "type": "string" }, "track_commit_timestamp": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "wal_keep_size": { "type": "string" @@ -1971,9 +3155,17 @@ "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", @@ -1998,9 +3190,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Reject non-secure connections to the database.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2021,25 +3221,57 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Edge Runtime service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "policy": { - "type": "string", - "enum": ["oneshot", "per_worker"], "description": "Configure the supported request policy.", - "default": "per_worker" + "default": "per_worker", + "anyOf": [ + { + "type": "string", + "enum": ["oneshot", "per_worker"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "inspector_port": { - "type": "number", "description": "Port to run the Edge Functions inspector on.", - "default": 8083 + "default": 8083, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "deno_version": { - "type": "number", "description": "The Deno major version to use.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secrets": { "type": "object", @@ -2064,14 +3296,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", @@ -2125,22 +3373,54 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local SMTP testing server.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the email testing server web interface.\n\nEmails sent with the local dev setup are monitored and available from the web interface.", - "default": 54324 + "default": 54324, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "smtp_port": { - "type": "number", - "description": "Optional SMTP port to expose for local testing." + "description": "Optional SMTP port to expose for local testing.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "pop3_port": { - "type": "number", - "description": "Optional POP3 port to expose for local testing." + "description": "Optional POP3 port to expose for local testing.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "admin_email": { "type": "string", @@ -2157,20 +3437,44 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Realtime service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "ip_version": { - "type": "string", - "enum": ["IPv4", "IPv6"], "description": "Bind realtime via either IPv4 or IPv6.", - "default": "IPv4" + "default": "IPv4", + "anyOf": [ + { + "type": "string", + "enum": ["IPv4", "IPv6"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_header_length": { - "type": "number", "description": "Maximum length of the HTTP header.", - "default": 4096 + "default": 4096, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2179,9 +3483,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Storage service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "file_size_limit": { "anyOf": [ @@ -2190,6 +3502,10 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, @@ -2197,9 +3513,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable image transformation.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2212,9 +3536,17 @@ "type": "object", "properties": { "public": { - "type": "boolean", "description": "Enable public access to the bucket.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "file_size_limit": { "anyOf": [ @@ -2223,6 +3555,10 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, @@ -2254,9 +3590,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Allow connections via S3 compatible clients.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2265,24 +3609,56 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable analytics buckets.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_namespaces": { - "type": "number", "description": "Maximum number of analytics namespaces.", - "default": 5 + "default": 5, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_tables": { - "type": "number", "description": "Maximum number of analytics tables.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_catalogs": { - "type": "number", "description": "Maximum number of analytics catalogs.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "buckets": { "anyOf": [ @@ -2320,19 +3696,43 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable vector buckets.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_buckets": { - "type": "number", "description": "Maximum number of vector buckets.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_indexes": { - "type": "number", "description": "Maximum number of vector indexes.", - "default": 5 + "default": 5, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "buckets": { "anyOf": [ @@ -2373,14 +3773,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Supabase Studio dashboard.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for Supabase Studio.", - "default": 54323 + "default": 54323, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "api_url": { "type": "string", @@ -2420,10 +3836,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", @@ -2454,12 +3878,28 @@ "type": "object", "properties": { "compute": { - "type": "boolean", - "description": "Enable the experimental compute command family." + "description": "Enable the experimental compute command family.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "stack": { - "type": "boolean", - "description": "Use the new local stack backend for start, stop, and status, and for --local targets of the db, migration, test db, gen types, inspect, and pull command families." + "description": "Use the new local stack backend for start, stop, and status, and for --local targets of the db, migration, test db, gen types, inspect, and pull command families.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "orioledb_version": { "type": "string", @@ -2491,9 +3931,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable experimental webhooks.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2502,9 +3950,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "declarative_schema_path": { "type": "string", @@ -2745,24 +4201,56 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Logflare service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to the local Logflare service.", - "default": 54327 + "default": 54327, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "backend": { - "type": "string", - "enum": ["postgres", "bigquery"], "description": "Configure one of the supported backends:\n\n- `postgres`\n- `bigquery`", - "default": "postgres" + "default": "postgres", + "anyOf": [ + { + "type": "string", + "enum": ["postgres", "bigquery"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "vector_port": { - "type": "number", - "description": "Port to the local syslog ingest service." + "description": "Port to the local syslog ingest service.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "gcp_project_id": { "type": "string", @@ -2783,14 +4271,30 @@ "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", @@ -2809,21 +4313,45 @@ "default": ["public", "extensions"] }, "max_rows": { - "type": "number", "description": "The maximum number of rows returned from a view, table, or stored procedure. Limits payload size for accidental or malicious requests.", - "default": 1000 + "default": 1000, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "auto_expose_new_tables": { - "type": "boolean", - "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off." + "description": "Controls whether newly-created tables, views, sequences and functions in the `public` schema by `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`) without explicit GRANTs. When unset, new entities are auto-exposed, matching the cloud default. Set to `false` to revoke the default Data API privileges so new entities require explicit GRANTs, matching a cloud project with the \"Default privileges for new entities\" toggle turned off.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "tls": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable HTTPS endpoints locally using a self-signed certificate.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "cert_path": { "type": "string", @@ -2847,9 +4375,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", @@ -2866,9 +4402,17 @@ "default": ["https://127.0.0.1:3000"] }, "jwt_expiry": { - "type": "number", "description": "How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).", - "default": 3600 + "default": 3600, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "jwt_issuer": { "type": "string", @@ -2879,45 +4423,101 @@ "description": "Path to the JWT signing keys file." }, "enable_refresh_token_rotation": { - "type": "boolean", "description": "If disabled, the refresh token will never expire.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "refresh_token_reuse_interval": { - "type": "number", "description": "Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_manual_linking": { - "type": "boolean", "description": "Allow/disallow testing manual linking of accounts.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups to your project.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_anonymous_sign_ins": { - "type": "boolean", "description": "Allow/disallow anonymous sign-ins to your project.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "minimum_password_length": { - "type": "number", "description": "Passwords shorter than this value will be rejected as weak.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "password_requirements": { - "type": "string", - "enum": [ - "", - "letters_digits", - "lower_upper_letters_digits", - "lower_upper_letters_digits_symbols" - ], "description": "Password character requirements.", - "default": "" + "default": "", + "anyOf": [ + { + "type": "string", + "enum": [ + "", + "letters_digits", + "lower_upper_letters_digits", + "lower_upper_letters_digits_symbols" + ] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "publishable_key": { "type": "string", @@ -2948,39 +4548,95 @@ "type": "object", "properties": { "email_sent": { - "type": "number", "description": "Number of emails that can be sent per hour.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sms_sent": { - "type": "number", "description": "Number of SMS messages that can be sent per hour.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "anonymous_users": { - "type": "number", "description": "Number of anonymous sign-ins that can be made per hour per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "token_refresh": { - "type": "number", "description": "Number of sessions that can be refreshed in a 5 minute interval per IP address.", - "default": 150 + "default": 150, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sign_in_sign_ups": { - "type": "number", "description": "Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "token_verifications": { - "type": "number", "description": "Number of OTP or magic link verifications that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "web3": { - "type": "number", "description": "Number of Web3 logins that can be made in a 5 minute interval per IP address.", - "default": 30 + "default": 30, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2989,14 +4645,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable CAPTCHA verification.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "provider": { - "type": "string", - "enum": ["hcaptcha", "turnstile"], - "description": "CAPTCHA provider to use." + "description": "CAPTCHA provider to use.", + "anyOf": [ + { + "type": "string", + "enum": ["hcaptcha", "turnstile"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secret": { "type": "string", @@ -3013,9 +4685,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the mfa verification hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3033,9 +4713,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the password verification hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3053,9 +4741,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the custom access token hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3073,9 +4769,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the send sms hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3093,9 +4797,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the send email hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3113,9 +4825,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable or disable the before user created hook.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "uri": { "type": "string", @@ -3139,14 +4859,30 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow TOTP enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow TOTP verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3155,19 +4891,43 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow phone enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow phone verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_length": { - "type": "number", "description": "The length of the OTP code.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "template": { "type": "string", @@ -3186,22 +4946,46 @@ "type": "object", "properties": { "enroll_enabled": { - "type": "boolean", "description": "Allow/disallow WebAuthn enrollment for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "verify_enabled": { - "type": "boolean", "description": "Allow/disallow WebAuthn verification for users.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false }, "max_enrolled_factors": { - "type": "number", "description": "The maximum number of MFA factors a user can enroll in.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3225,24 +5009,56 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via email to your project.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "double_confirm_changes": { - "type": "boolean", "description": "If enabled, a user will be required to confirm any email change on both the old and new email addresses.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_confirmations": { - "type": "boolean", "description": "If enabled, users need to confirm their email address before signing in.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secure_password_change": { - "type": "boolean", "description": "If enabled, users will need to reauthenticate or have logged in recently to change their password.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_frequency": { "type": "string", @@ -3250,30 +5066,62 @@ "default": "1s" }, "otp_length": { - "type": "number", "description": "Number of characters used in the email OTP.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_expiry": { - "type": "number", "description": "Number of seconds before the email OTP expires.", - "default": 3600 + "default": 3600, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "smtp": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable SMTP for email delivery.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "host": { "type": "string", "description": "Hostname or IP address of the SMTP server." }, "port": { - "type": "number", - "description": "Port number of the SMTP server." + "description": "Port number of the SMTP server.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "user": { "type": "string", @@ -3340,9 +5188,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the notification email.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "subject": { "type": "string", @@ -3377,14 +5233,30 @@ "type": "object", "properties": { "enable_signup": { - "type": "boolean", "description": "Allow/disallow new user signups via SMS to your project.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "enable_confirmations": { - "type": "boolean", "description": "If enabled, users need to confirm their phone number before signing in.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "template": { "type": "string", @@ -3397,22 +5269,46 @@ "default": "5s" }, "otp_length": { - "type": "number", "description": "Number of characters used in the SMS OTP.", - "default": 6 + "default": 6, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "otp_expiry": { - "type": "number", "description": "Number of seconds before the SMS OTP expires.", - "default": 60 + "default": 60, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "twilio": { "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Twilio provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "account_sid": { "type": "string", @@ -3441,9 +5337,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Twilio Verify provider for phone verification.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "account_sid": { "type": "string", @@ -3465,9 +5369,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable MessageBird provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "originator": { "type": "string", @@ -3485,9 +5397,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Textlocal provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "sender": { "type": "string", @@ -3505,9 +5425,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable/disable Vonage provider for phone login.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "from": { "type": "string", @@ -3542,9 +5470,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Apple OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3568,14 +5504,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3584,9 +5536,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Azure OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3610,14 +5570,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3626,9 +5602,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Bitbucket OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3652,14 +5636,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3668,9 +5668,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Discord OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3694,14 +5702,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3710,9 +5734,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Facebook OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3736,14 +5768,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3752,9 +5800,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Figma OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3778,14 +5834,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3794,9 +5866,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the GitHub OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3820,14 +5900,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3836,9 +5932,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the GitLab OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3862,14 +5966,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3878,9 +5998,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Google OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3904,14 +6032,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3920,9 +6064,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Kakao OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3946,14 +6098,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -3962,9 +6130,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Keycloak OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -3989,14 +6165,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4005,9 +6197,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the LinkedIn OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4031,14 +6231,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4047,9 +6263,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Notion OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4073,14 +6297,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4089,9 +6329,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Twitch OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4115,14 +6363,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4131,9 +6395,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Twitter OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4157,14 +6429,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4173,9 +6461,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the X OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4199,14 +6495,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4215,9 +6527,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Slack OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4241,14 +6561,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4257,9 +6593,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Spotify OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4283,14 +6627,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4299,9 +6659,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the WorkOS OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4325,14 +6693,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4341,9 +6725,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use the Zoom OAuth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "client_id": { "type": "string", @@ -4367,14 +6759,30 @@ "default": "" }, "skip_nonce_check": { - "type": "boolean", "description": "If true, the nonce check will be skipped.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "email_optional": { - "type": "boolean", "description": "If true, authentication succeeds when the provider does not return an email address.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4389,9 +6797,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this Web3 provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4400,9 +6816,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this Web3 provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4414,9 +6838,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable OAuth server functionality.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "authorization_url_path": { "type": "string", @@ -4424,9 +6856,17 @@ "default": "/oauth/consent" }, "allow_dynamic_registration": { - "type": "boolean", "description": "Allow dynamic client registration.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4438,9 +6878,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "project_id": { "type": "string", @@ -4453,9 +6901,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "tenant": { "type": "string", @@ -4472,9 +6928,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "user_pool_id": { "type": "string", @@ -4491,9 +6955,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "domain": { "type": "string", @@ -4506,9 +6978,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable this third-party auth provider.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "issuer_url": { "type": "string", @@ -4527,14 +7007,30 @@ "type": "object", "properties": { "port": { - "type": "number", "description": "Port to use for the local database URL.", - "default": 54322 + "default": 54322, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "shadow_port": { - "type": "number", "description": "Port used by db diff command to initialize the shadow database.", - "default": 54320 + "default": 54320, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "health_timeout": { "type": "string", @@ -4542,9 +7038,17 @@ "default": "2m" }, "major_version": { - "type": "number", "description": "The database major version to use. This has to be the same as your remote database's.", - "default": 17 + "default": 17, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "orioledb_version": { "type": "string", @@ -4554,30 +7058,70 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local PgBouncer service.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the local connection pooler.", - "default": 54329 + "default": 54329, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "pool_mode": { - "type": "string", - "enum": ["transaction", "session"], "description": "Specifies when a server connection can be reused by other clients.", - "default": "transaction" + "default": "transaction", + "anyOf": [ + { + "type": "string", + "enum": ["transaction", "session"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "default_pool_size": { - "type": "number", "description": "How many server connections to allow per user/database pair.", - "default": 20 + "default": 20, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_client_conn": { - "type": "number", "description": "Maximum number of client connections allowed.", - "default": 100 + "default": 100, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4586,9 +7130,17 @@ "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", @@ -4606,9 +7158,17 @@ "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", @@ -4635,22 +7195,70 @@ "type": "string" }, "max_connections": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_locks_per_transaction": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_maintenance_workers": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_workers": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_parallel_workers_per_gather": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_replication_slots": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_slot_wal_keep_size": { "type": "string" @@ -4665,15 +7273,39 @@ "type": "string" }, "max_wal_senders": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_worker_processes": { - "type": "number" + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "session_replication_role": { - "type": "string", - "enum": ["origin", "replica", "local"], - "description": "Session replication role." + "description": "Session replication role.", + "anyOf": [ + { + "type": "string", + "enum": ["origin", "replica", "local"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "shared_buffers": { "type": "string" @@ -4685,7 +7317,15 @@ "type": "string" }, "track_commit_timestamp": { - "type": "boolean" + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "wal_keep_size": { "type": "string" @@ -4703,9 +7343,17 @@ "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", @@ -4730,9 +7378,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Reject non-secure connections to the database.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4753,25 +7409,57 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Edge Runtime service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "policy": { - "type": "string", - "enum": ["oneshot", "per_worker"], "description": "Configure the supported request policy.", - "default": "per_worker" + "default": "per_worker", + "anyOf": [ + { + "type": "string", + "enum": ["oneshot", "per_worker"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "inspector_port": { - "type": "number", "description": "Port to run the Edge Functions inspector on.", - "default": 8083 + "default": 8083, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "deno_version": { - "type": "number", "description": "The Deno major version to use.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "secrets": { "type": "object", @@ -4796,14 +7484,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", @@ -4857,22 +7561,54 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local SMTP testing server.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for the email testing server web interface.\n\nEmails sent with the local dev setup are monitored and available from the web interface.", - "default": 54324 + "default": 54324, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "smtp_port": { - "type": "number", - "description": "Optional SMTP port to expose for local testing." + "description": "Optional SMTP port to expose for local testing.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "pop3_port": { - "type": "number", - "description": "Optional POP3 port to expose for local testing." + "description": "Optional POP3 port to expose for local testing.", + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "admin_email": { "type": "string", @@ -4889,20 +7625,44 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Realtime service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "ip_version": { - "type": "string", - "enum": ["IPv4", "IPv6"], "description": "Bind realtime via either IPv4 or IPv6.", - "default": "IPv4" + "default": "IPv4", + "anyOf": [ + { + "type": "string", + "enum": ["IPv4", "IPv6"] + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_header_length": { - "type": "number", "description": "Maximum length of the HTTP header.", - "default": 4096 + "default": 4096, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4911,9 +7671,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Storage service.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "file_size_limit": { "anyOf": [ @@ -4922,6 +7690,10 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, @@ -4929,9 +7701,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable image transformation.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4944,9 +7724,17 @@ "type": "object", "properties": { "public": { - "type": "boolean", "description": "Enable public access to the bucket.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "file_size_limit": { "anyOf": [ @@ -4955,6 +7743,10 @@ }, { "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" } ] }, @@ -4986,9 +7778,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Allow connections via S3 compatible clients.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4997,24 +7797,56 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable analytics buckets.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_namespaces": { - "type": "number", "description": "Maximum number of analytics namespaces.", - "default": 5 + "default": 5, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_tables": { - "type": "number", "description": "Maximum number of analytics tables.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_catalogs": { - "type": "number", "description": "Maximum number of analytics catalogs.", - "default": 2 + "default": 2, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "buckets": { "anyOf": [ @@ -5052,19 +7884,43 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable vector buckets.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_buckets": { - "type": "number", "description": "Maximum number of vector buckets.", - "default": 10 + "default": 10, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "max_indexes": { - "type": "number", "description": "Maximum number of vector indexes.", - "default": 5 + "default": 5, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "buckets": { "anyOf": [ @@ -5105,14 +7961,30 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable the local Supabase Studio dashboard.", - "default": true + "default": true, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "port": { - "type": "number", "description": "Port to use for Supabase Studio.", - "default": 54323 + "default": 54323, + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "api_url": { "type": "string", @@ -5152,10 +8024,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", @@ -5186,12 +8066,28 @@ "type": "object", "properties": { "compute": { - "type": "boolean", - "description": "Enable the experimental compute command family." + "description": "Enable the experimental compute command family.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "stack": { - "type": "boolean", - "description": "Use the new local stack backend for start, stop, and status, and for --local targets of the db, migration, test db, gen types, inspect, and pull command families." + "description": "Use the new local stack backend for start, stop, and status, and for --local targets of the db, migration, test db, gen types, inspect, and pull command families.", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "orioledb_version": { "type": "string", @@ -5223,9 +8119,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Enable experimental webhooks.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -5234,9 +8138,17 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", - "default": false + "default": false, + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "declarative_schema_path": { "type": "string", diff --git a/packages/config/AGENTS.md b/packages/config/AGENTS.md index f1c0b4eb45..ebe72e54d2 100644 --- a/packages/config/AGENTS.md +++ b/packages/config/AGENTS.md @@ -50,7 +50,7 @@ artifacts (`./schema.json`, `./project-schema.json`). `apps/cli` needs them, and internal-only keeps the published semver surface unchanged). Anything here can change or vanish in any release. - `@supabase/config/schema.json` — generated JSON Schema (draft 2020-12) for `CliConfig` (a - `dist/` build output). + `dist/` build output); number, integer, boolean and enum fields also accept `env(VAR)` strings. - `@supabase/config/project-schema.json` (CLI-2234) — generated JSON Schema (draft 2020-12) for `ProjectConfig`, derived from `ProjectConfigSchema` (`src/project-config/project-schema.ts`); a `dist/` build output alongside `schema.json`. diff --git a/packages/config/package.json b/packages/config/package.json index 8302128046..8fffa6469a 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -84,6 +84,7 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@vitest/coverage-v8": "catalog:", + "ajv": "^8.20.0", "effect": "catalog:", "semantic-release": "catalog:", "typescript": "catalog:", diff --git a/packages/config/scripts/build-artifacts.unit.test.ts b/packages/config/scripts/build-artifacts.unit.test.ts index f8da54e665..3a7a50f404 100644 --- a/packages/config/scripts/build-artifacts.unit.test.ts +++ b/packages/config/scripts/build-artifacts.unit.test.ts @@ -75,7 +75,11 @@ describe("published JSON Schema artifacts", () => { throw new Error("expected properties.api.properties.max_rows to be an object"); } const maxRows = apiProperties["max_rows"]; - expect(maxRows["type"]).toBe("number"); + const anyOf = maxRows["anyOf"]; + if (!Array.isArray(anyOf) || !isRecord(anyOf[0])) { + throw new Error("expected max_rows to be an anyOf of the number and an env() string"); + } + expect(anyOf[0]["type"]).toBe("number"); expect(typeof maxRows["description"]).toBe("string"); expect(maxRows["default"]).toBe(1000); }); diff --git a/packages/config/src/base.ts b/packages/config/src/base.ts index 143762fd7f..6f6af2e211 100644 --- a/packages/config/src/base.ts +++ b/packages/config/src/base.ts @@ -83,7 +83,10 @@ export const CliConfigSchema = Schema.Struct({ }); export function toCliConfigJsonSchema() { - return toPublishedJsonSchemaDocument(CliConfigSchema, { onExcessProperty: "error" }); + return toPublishedJsonSchemaDocument(CliConfigSchema, { + onExcessProperty: "error", + acceptEnvReferences: true, + }); } export type CliConfig = typeof CliConfigSchema.Type; diff --git a/packages/config/src/json-schema-document.ts b/packages/config/src/json-schema-document.ts index 2fbc1cbe66..9d1bb1e231 100644 --- a/packages/config/src/json-schema-document.ts +++ b/packages/config/src/json-schema-document.ts @@ -1,23 +1,44 @@ 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", "deprecated"]); +const DATA_KEYWORDS = new Set(["default", "examples", "enum", "const"]); +const SCHEMA_MAP_KEYWORDS = new Set([ + "properties", + "patternProperties", + "$defs", + "definitions", + "dependentSchemas", +]); /** * 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 and string-enum leaves also accept `env(VAR)` + * strings. */ export function toPublishedJsonSchemaDocument( schema: Schema.Constraint, - options: { readonly onExcessProperty: "ignore" | "error" }, + options: { + readonly onExcessProperty: "ignore" | "error"; + readonly acceptEnvReferences?: boolean; + }, ) { const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: options.onExcessProperty, includeAnnotationKey: (key) => key === "x-secret", }); + const root = openPatternKeyedRecords(document.schema); + const definitions = openPatternKeyedRecords(document.definitions); return { $schema: "https://json-schema.org/draft/2020-12/schema", - ...openPatternKeyedRecords(document.schema), - ...(Object.keys(document.definitions).length > 0 - ? { $defs: openPatternKeyedRecords(document.definitions) } + ...(options.acceptEnvReferences ? acceptEnvReferences(root) : root), + ...(Object.keys(definitions).length > 0 + ? { $defs: options.acceptEnvReferences ? mapSchemas(definitions) : definitions } : {}), }; } @@ -37,3 +58,65 @@ 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 { + return ( + isNode(node) && + Object.keys(node).length === 2 && + node["type"] === "string" && + node["pattern"] === ENV_PATTERN + ); +} + +function isEnvReferenceTarget(node: unknown): node is Node { + if (!isNode(node) || typeof node["type"] !== "string") return false; + return ( + COERCED_TYPES.has(node["type"]) || (node["type"] === "string" && Array.isArray(node["enum"])) + ); +} + +function isNode(value: unknown): value is Node { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/config/src/json-schema-document.unit.test.ts b/packages/config/src/json-schema-document.unit.test.ts index 99eba9d5ee..1be9411173 100644 --- a/packages/config/src/json-schema-document.unit.test.ts +++ b/packages/config/src/json-schema-document.unit.test.ts @@ -1,8 +1,15 @@ +import type { ErrorObject, ValidateFunction } from "ajv"; +import Ajv2020 from "ajv/dist/2020"; import { Schema } from "effect"; import { describe, expect, test } from "vitest"; -import { toCliConfigJsonSchema } from "./base.ts"; +import { CliConfigSchema, toCliConfigJsonSchema } from "./base.ts"; import { toPublishedJsonSchemaDocument } from "./json-schema-document.ts"; -import { env } from "./lib/env.ts"; +import { + ENV_PATTERN, + env, + interpolateEnvReferencesAgainstSchema, + isEnvReference, +} from "./lib/env.ts"; import { secretPathPatterns } from "./lib/secret-paths.ts"; import { toProjectConfigJsonSchema } from "./project-config/project-schema.ts"; @@ -17,6 +24,97 @@ const at = (node: unknown, ...path: ReadonlyArray): Node => { const isNode = (value: unknown): value is Node => typeof value === "object" && value !== null && !Array.isArray(value); +const DATA_KEYWORDS = new Set(["default", "examples", "enum", "const"]); +const SCHEMA_MAP_KEYWORDS = new Set(["properties", "patternProperties", "$defs"]); +const COERCED_TYPES = new Set(["number", "integer", "boolean"]); + +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 isEnvReferenceLeaf = (node: Node): boolean => + typeof node.type === "string" && + (COERCED_TYPES.has(node.type) || (node.type === "string" && Array.isArray(node.enum))); + +const collectEnvBranches = (document: unknown): Array => { + const found: Array = []; + visitSchemas(document, (node, _path, siblings) => { + if (siblings !== undefined && isEnvBranch(node)) found.push(node); + }); + return found; +}; + +const findEnvReferenceGaps = (document: unknown) => { + const gaps: Array = []; + let leaves = 0; + visitSchemas(document, (node, path, siblings) => { + const where = path.join("."); + if (isEnvReferenceLeaf(node)) { + leaves += 1; + const branches = siblings?.filter(isEnvBranch).length ?? 0; + if (branches !== 1) gaps.push(`${where}: ${branches} env branches next to ${node.type}`); + } + if (Array.isArray(node.anyOf) && node.anyOf.filter(isEnvBranch).length > 1) { + gaps.push(`${where}: more than one env branch in the same anyOf`); + } + }); + return { gaps, leaves }; +}; + +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 collectSecretPaths = (document: unknown): Array => { const found = new Set(); const visit = (node: unknown, path: ReadonlyArray): void => { @@ -90,3 +188,396 @@ describe("published JSON Schema documents", () => { expect(JSON.stringify(toCliConfigJsonSchema())).not.toContain("x-env-deferred"); }); }); + +describe("env() references in the published CLI JSON Schema", () => { + const cli = toCliConfigJsonSchema(); + const validateCli = compile(cli); + + const baseline = { + project_id: "demo", + api: { port: 54321, max_rows: 1000 }, + auth: { enable_signup: true }, + db: { settings: { max_connections: 100 } }, + compute: { web: { instances: 2 } }, + storage: { file_size_limit: "50MiB" }, + remotes: { staging: { api: { max_rows: 500 }, compute: { web: { instances: 4 } } } }, + }; + + describe("validation", () => { + test("a document of plain literals validates", () => { + expect(errorsFor(validateCli, baseline)).toEqual([]); + }); + + test.each([ + ["api.max_rows", { api: { max_rows: "env(MAX_ROWS)" } }], + ["api.port", { api: { port: "env(API_PORT)" } }], + ["auth.enable_signup", { auth: { enable_signup: "env(ENABLE_SIGNUP)" } }], + ["db.settings.max_connections", { db: { settings: { max_connections: "env(MAX_CONN)" } } }], + ["compute..instances", { compute: { web: { instances: "env(WEB_INSTANCES)" } } }], + ["storage.file_size_limit", { storage: { file_size_limit: "env(FILE_SIZE_LIMIT)" } }], + ["db.pooler.pool_mode", { db: { pooler: { pool_mode: "env(POOL_MODE)" } } }], + ["realtime.ip_version", { realtime: { ip_version: "env(IP_VERSION)" } }], + ["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) }); + }); + + test("every number, integer, boolean and string-enum node sits next to exactly one env branch", () => { + const { gaps, leaves } = findEnvReferenceGaps(cli); + const typeKeywords = + JSON.stringify(cli).match(/"type":"(?:number|integer|boolean|string","enum)"/g) ?? []; + expect(leaves).toBeGreaterThan(0); + expect(leaves).toBe(typeKeywords.length); + expect(gaps).toEqual([]); + }); + }); + + 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("a document using env() on numeric and boolean fields loads through the real loader", () => { + const document = { + api: { port: "env(API_PORT)", max_rows: "env(MAX_ROWS)" }, + auth: { enable_signup: "env(ENABLE_SIGNUP)" }, + db: { + settings: { max_connections: "env(MAX_CONN)" }, + pooler: { pool_mode: "env(POOL_MODE)" }, + }, + compute: { web: { instances: "env(WEB_INSTANCES)" } }, + }; + expect(errorsFor(validateCli, document)).toEqual([]); + + const resolved = interpolateEnvReferencesAgainstSchema( + document, + { + API_PORT: "54329", + MAX_ROWS: "250", + ENABLE_SIGNUP: "false", + MAX_CONN: "80", + WEB_INSTANCES: "3", + POOL_MODE: "session", + }, + CliConfigSchema, + { goViperCompat: true }, + ); + const decoded = Schema.decodeUnknownSync(CliConfigSchema)(resolved); + expect(decoded).toMatchObject({ + api: { port: 54329, max_rows: 250 }, + auth: { enable_signup: false }, + db: { settings: { max_connections: 80 }, pooler: { pool_mode: "session" } }, + compute: { web: { instances: 3 } }, + }); + }); + }); +}); + +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()]) }), true); + expect(at(document, "properties", "count").anyOf).toEqual([{ type: "number" }, envBranch]); + }); + + 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("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(findEnvReferenceGaps(project).leaves).toBeGreaterThan(0); + expect(collectEnvBranches(project)).toEqual([]); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8673924f0..e0be1578fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,6 +584,9 @@ importers: '@vitest/coverage-v8': specifier: 'catalog:' version: 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vitest@5.0.0) + ajv: + specifier: ^8.20.0 + version: 8.20.0 effect: specifier: 'catalog:' version: 4.0.2 From b95df88c6e6c865df809bae848c26ed522af1e9f Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 26 Oct 2026 16:42:46 +0000 Subject: [PATCH 2/4] feat(config): accept env() on string-list fields and test schema against the loader String-list fields such as auth.additional_redirect_urls now accept an env() reference in place of the whole list, matching the loader's comma-split coercion. The copied-rule test is replaced by an agreement test that, for every leaf in the CLI schema, checks the published schema accepts env() exactly when the loader resolves it. Unused keyword entries are dropped. --- apps/docs/public/cli/config.schema.json | 388 +++++++++++++----- packages/config/AGENTS.md | 2 +- packages/config/src/json-schema-document.ts | 24 +- .../src/json-schema-document.unit.test.ts | 184 ++++++--- 4 files changed, 407 insertions(+), 191 deletions(-) diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 67a81f8623..e64d6aeb24 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -109,20 +109,36 @@ ] }, "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": { "description": "The maximum number of rows returned from a view, table, or stored procedure. Limits payload size for accidental or malicious requests.", @@ -205,13 +221,21 @@ "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": { "description": "How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).", @@ -2955,13 +2979,21 @@ ] }, "schema_paths": { - "type": "array", - "items": { - "type": "string", - "description": "Schema file path, directory, or glob relative to the supabase directory." - }, "description": "Ordered list of schema files, directories, or glob patterns that describe your database.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Schema file path, directory, or glob relative to the supabase directory." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -2983,13 +3015,21 @@ ] }, "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 @@ -3168,20 +3208,36 @@ ] }, "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 @@ -3332,13 +3388,21 @@ "default": "" }, "static_files": { - "type": "array", - "items": { - "type": "string", - "description": "Static file glob for the function." - }, "description": "Static files to bundle with the function.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Static file glob for the function." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "env": { "type": "object", @@ -3563,13 +3627,21 @@ ] }, "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", @@ -3855,12 +3927,20 @@ "examples": ["packages/api"] }, "exclude": { - "type": "array", - "items": { - "type": "string" - }, "description": "Patterns for paths to leave out of the uploaded build context, read the way\n`.gitignore` reads them: a pattern without `/` matches that name at any depth,\none with `/` is anchored at the compute's source directory, a trailing `/`\nmatches directories only, and `**` spans directories. Excluding a directory\nexcludes everything under it; re-inclusion (`!`) is not supported. Only the\npatterns recorded here are excluded, so a compute with no list uploads its source\ndirectory whole.", - "examples": [["node_modules", ".env"]] + "examples": [["node_modules", ".env"]], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -4297,20 +4377,36 @@ ] }, "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": { "description": "The maximum number of rows returned from a view, table, or stored procedure. Limits payload size for accidental or malicious requests.", @@ -4393,13 +4489,21 @@ "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": { "description": "How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one week).", @@ -7143,13 +7247,21 @@ ] }, "schema_paths": { - "type": "array", - "items": { - "type": "string", - "description": "Schema file path, directory, or glob relative to the supabase directory." - }, "description": "Ordered list of schema files, directories, or glob patterns that describe your database.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Schema file path, directory, or glob relative to the supabase directory." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false @@ -7171,13 +7283,21 @@ ] }, "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 @@ -7356,20 +7476,36 @@ ] }, "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 @@ -7520,13 +7656,21 @@ "default": "" }, "static_files": { - "type": "array", - "items": { - "type": "string", - "description": "Static file glob for the function." - }, "description": "Static files to bundle with the function.", - "default": [] + "default": [], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "Static file glob for the function." + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] }, "env": { "type": "object", @@ -7751,13 +7895,21 @@ ] }, "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", @@ -8043,12 +8195,20 @@ "examples": ["packages/api"] }, "exclude": { - "type": "array", - "items": { - "type": "string" - }, "description": "Patterns for paths to leave out of the uploaded build context, read the way\n`.gitignore` reads them: a pattern without `/` matches that name at any depth,\none with `/` is anchored at the compute's source directory, a trailing `/`\nmatches directories only, and `**` spans directories. Excluding a directory\nexcludes everything under it; re-inclusion (`!`) is not supported. Only the\npatterns recorded here are excluded, so a compute with no list uploads its source\ndirectory whole.", - "examples": [["node_modules", ".env"]] + "examples": [["node_modules", ".env"]], + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string", + "pattern": "^env\\((.*)\\)$" + } + ] } }, "additionalProperties": false diff --git a/packages/config/AGENTS.md b/packages/config/AGENTS.md index ebe72e54d2..46ae61ee30 100644 --- a/packages/config/AGENTS.md +++ b/packages/config/AGENTS.md @@ -50,7 +50,7 @@ artifacts (`./schema.json`, `./project-schema.json`). `apps/cli` needs them, and internal-only keeps the published semver surface unchanged). Anything here can change or vanish in any release. - `@supabase/config/schema.json` — generated JSON Schema (draft 2020-12) for `CliConfig` (a - `dist/` build output); number, integer, boolean and enum fields also accept `env(VAR)` strings. + `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/src/json-schema-document.ts b/packages/config/src/json-schema-document.ts index 9d1bb1e231..d524f84427 100644 --- a/packages/config/src/json-schema-document.ts +++ b/packages/config/src/json-schema-document.ts @@ -4,22 +4,16 @@ 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", "deprecated"]); +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", - "$defs", - "definitions", - "dependentSchemas", -]); +const SCHEMA_MAP_KEYWORDS = new Set(["properties", "patternProperties"]); /** * Renders a published JSON Schema document, carrying the `x-secret` marker through and keeping * pattern-keyed records open: the generator writes a boolean `additionalProperties` on them * (effect 4.0) with no per-schema setting, while the published documents never carried one there. - * With `acceptEnvReferences`, number, integer, boolean and string-enum leaves also accept `env(VAR)` - * strings. + * With `acceptEnvReferences`, number, integer, boolean, string-enum and string-list leaves also + * accept the `env(VAR)` references the loader resolves. */ export function toPublishedJsonSchemaDocument( schema: Schema.Constraint, @@ -112,9 +106,13 @@ function isEnvReferenceBranch(node: unknown): boolean { function isEnvReferenceTarget(node: unknown): node is Node { if (!isNode(node) || typeof node["type"] !== "string") return false; - return ( - COERCED_TYPES.has(node["type"]) || (node["type"] === "string" && Array.isArray(node["enum"])) - ); + if (COERCED_TYPES.has(node["type"])) return true; + if (node["type"] === "string") return !isFreeFormString(node); + return node["type"] === "array" && isFreeFormString(node["items"]); +} + +function isFreeFormString(node: unknown): boolean { + return isNode(node) && node["type"] === "string" && !("enum" in node); } function isNode(value: unknown): value is Node { diff --git a/packages/config/src/json-schema-document.unit.test.ts b/packages/config/src/json-schema-document.unit.test.ts index 1be9411173..05ffcede0f 100644 --- a/packages/config/src/json-schema-document.unit.test.ts +++ b/packages/config/src/json-schema-document.unit.test.ts @@ -1,6 +1,6 @@ import type { ErrorObject, ValidateFunction } from "ajv"; import Ajv2020 from "ajv/dist/2020"; -import { Schema } from "effect"; +import { Result, Schema, SchemaIssue } from "effect"; import { describe, expect, test } from "vitest"; import { CliConfigSchema, toCliConfigJsonSchema } from "./base.ts"; import { toPublishedJsonSchemaDocument } from "./json-schema-document.ts"; @@ -26,7 +26,6 @@ const isNode = (value: unknown): value is Node => const DATA_KEYWORDS = new Set(["default", "examples", "enum", "const"]); const SCHEMA_MAP_KEYWORDS = new Set(["properties", "patternProperties", "$defs"]); -const COERCED_TYPES = new Set(["number", "integer", "boolean"]); type Path = ReadonlyArray; @@ -65,10 +64,6 @@ const isEnvBranch = (node: unknown): boolean => typeof node.pattern === "string" && Object.keys(node).length === 2; -const isEnvReferenceLeaf = (node: Node): boolean => - typeof node.type === "string" && - (COERCED_TYPES.has(node.type) || (node.type === "string" && Array.isArray(node.enum))); - const collectEnvBranches = (document: unknown): Array => { const found: Array = []; visitSchemas(document, (node, _path, siblings) => { @@ -77,23 +72,6 @@ const collectEnvBranches = (document: unknown): Array => { return found; }; -const findEnvReferenceGaps = (document: unknown) => { - const gaps: Array = []; - let leaves = 0; - visitSchemas(document, (node, path, siblings) => { - const where = path.join("."); - if (isEnvReferenceLeaf(node)) { - leaves += 1; - const branches = siblings?.filter(isEnvBranch).length ?? 0; - if (branches !== 1) gaps.push(`${where}: ${branches} env branches next to ${node.type}`); - } - if (Array.isArray(node.anyOf) && node.anyOf.filter(isEnvBranch).length > 1) { - gaps.push(`${where}: more than one env branch in the same anyOf`); - } - }); - return { gaps, leaves }; -}; - const patternOf = (node: Node): string => { if (typeof node.pattern !== "string") throw new Error("expected the node to carry a pattern"); return node.pattern; @@ -115,6 +93,93 @@ const failingPaths = (validate: ValidateFunction, value: unknown): ReadonlyArray const envPatternOf = (document: Node): string => patternOf(at(document, "properties", "api", "properties", "max_rows", "anyOf", 1)); +const RECORD_KEYS = ["sample", "SAMPLE"]; + +const collectLeaves = (node: unknown, path: Path = []): Array => { + if (!isNode(node)) return []; + const { properties, patternProperties, additionalProperties, anyOf, items } = node; + if (isNode(properties) || isNode(patternProperties) || isNode(additionalProperties)) { + return [ + ...Object.entries(isNode(properties) ? properties : {}).flatMap(([name, child]) => + collectLeaves(child, [...path, name]), + ), + ...Object.entries(isNode(patternProperties) ? patternProperties : {}).flatMap( + ([pattern, child]) => { + const key = RECORD_KEYS.find((candidate) => new RegExp(pattern, "u").test(candidate)); + return key === undefined ? [] : collectLeaves(child, [...path, key]); + }, + ), + ...collectLeaves(additionalProperties, [...path, "sample"]), + ]; + } + if (Array.isArray(anyOf) && anyOf.some((member) => isNode(member) && member.type === "object")) { + return anyOf.flatMap((member) => collectLeaves(member, path)); + } + if (isNode(items) && items.type === "object") return collectLeaves(items, [...path, 0]); + return [[path, node]]; +}; + +const envValueFor = (leaf: Node): string | undefined => { + const typed = Array.isArray(leaf.anyOf) ? at(leaf, "anyOf", 0) : leaf; + switch (typed.type) { + case "boolean": + return "false"; + case "number": + case "integer": + return "1"; + case "array": + return "a,b"; + case "string": + return Array.isArray(typed.enum) ? String(typed.enum.find((value) => value !== "")) : "x"; + default: + return undefined; + } +}; + +const documentAt = (path: Path, value: unknown): unknown => + path.reduceRight( + (child, key) => (typeof key === "number" ? [child] : { [key]: child }), + value, + ); + +const isAtOrBelow = (issuePath: ReadonlyArray, path: Path): boolean => + path.every((key, index) => String(issuePath[index]) === String(key)); + +const schemaAcceptsAt = (validate: ValidateFunction, path: Path, value: unknown): boolean => + !failingPaths(validate, documentAt(path, value)).some((pointer) => + isAtOrBelow(pointer.split("/").slice(1), path), + ); + +// Checks are off so only type acceptance is compared, not value rules such as port ranges. +const decodeCli = Schema.decodeUnknownResult(CliConfigSchema, { + errors: "all", + disableChecks: true, +}); +const toIssues = SchemaIssue.makeFormatterStandardSchemaV1(); + +const loaderAcceptsAt = ( + path: Path, + value: unknown, + env: Readonly>, +): boolean => { + const resolved = interpolateEnvReferencesAgainstSchema( + documentAt(path, value), + env, + CliConfigSchema, + { + goViperCompat: true, + }, + ); + const result = decodeCli(resolved); + if (Result.isSuccess(result)) return true; + return !toIssues(result.failure.issue).issues.some((issue) => + isAtOrBelow( + (issue.path ?? []).map((segment) => (typeof segment === "object" ? segment.key : segment)), + path, + ), + ); +}; + const collectSecretPaths = (document: unknown): Array => { const found = new Set(); const visit = (node: unknown, path: ReadonlyArray): void => { @@ -217,6 +282,10 @@ describe("env() references in the published CLI JSON Schema", () => { ["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", @@ -317,15 +386,6 @@ describe("env() references in the published CLI JSON Schema", () => { expect(at(fileSizeLimit, "anyOf", 1)).toEqual({ type: "number" }); expect(at(fileSizeLimit, "anyOf", 2)).toEqual({ type: "string", pattern: envPatternOf(cli) }); }); - - test("every number, integer, boolean and string-enum node sits next to exactly one env branch", () => { - const { gaps, leaves } = findEnvReferenceGaps(cli); - const typeKeywords = - JSON.stringify(cli).match(/"type":"(?:number|integer|boolean|string","enum)"/g) ?? []; - expect(leaves).toBeGreaterThan(0); - expect(leaves).toBe(typeKeywords.length); - expect(gaps).toEqual([]); - }); }); describe("agreement with the loader's env() matching", () => { @@ -374,38 +434,24 @@ describe("env() references in the published CLI JSON Schema", () => { expect(new Set(branches.map(patternOf))).toEqual(new Set([envPatternOf(cli)])); }); - test("a document using env() on numeric and boolean fields loads through the real loader", () => { - const document = { - api: { port: "env(API_PORT)", max_rows: "env(MAX_ROWS)" }, - auth: { enable_signup: "env(ENABLE_SIGNUP)" }, - db: { - settings: { max_connections: "env(MAX_CONN)" }, - pooler: { pool_mode: "env(POOL_MODE)" }, - }, - compute: { web: { instances: "env(WEB_INSTANCES)" } }, - }; - expect(errorsFor(validateCli, document)).toEqual([]); + 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 resolved = interpolateEnvReferencesAgainstSchema( - document, - { - API_PORT: "54329", - MAX_ROWS: "250", - ENABLE_SIGNUP: "false", - MAX_CONN: "80", - WEB_INSTANCES: "3", - POOL_MODE: "session", - }, - CliConfigSchema, - { goViperCompat: true }, - ); - const decoded = Schema.decodeUnknownSync(CliConfigSchema)(resolved); - expect(decoded).toMatchObject({ - api: { port: 54329, max_rows: 250 }, - auth: { enable_signup: false }, - db: { settings: { max_connections: 80 }, pooler: { pool_mode: "session" } }, - compute: { web: { instances: 3 } }, + 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([]); }); }); }); @@ -561,6 +607,19 @@ describe("toPublishedJsonSchemaDocument acceptEnvReferences", () => { ); }); + 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" }); @@ -577,7 +636,6 @@ describe("toPublishedJsonSchemaDocument acceptEnvReferences", () => { const maxRows = at(project, "properties", "api", "properties", "max_rows"); expect(maxRows.type).toBe("number"); expect(maxRows).not.toHaveProperty("anyOf"); - expect(findEnvReferenceGaps(project).leaves).toBeGreaterThan(0); expect(collectEnvBranches(project)).toEqual([]); }); }); From d11327d3f7f50040091b5789010fe1658e2f4e0e Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 26 Oct 2026 16:51:46 +0000 Subject: [PATCH 3/4] test(config): give the schema-loader agreement test room on slow runners --- packages/config/src/json-schema-document.unit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/src/json-schema-document.unit.test.ts b/packages/config/src/json-schema-document.unit.test.ts index 05ffcede0f..2773d739c8 100644 --- a/packages/config/src/json-schema-document.unit.test.ts +++ b/packages/config/src/json-schema-document.unit.test.ts @@ -452,7 +452,7 @@ describe("env() references in the published CLI JSON Schema", () => { } expect(leaves.length).toBeGreaterThan(500); expect(mismatches).toEqual([]); - }); + }, 60_000); }); }); From c19ef9c3d8805d203d1d85c5ba2ee5111f0fa698 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 27 Oct 2026 11:48:48 +0000 Subject: [PATCH 4/4] fix(config): recognise annotated env() union members as env branches --- packages/config/src/json-schema-document.ts | 9 +++------ packages/config/src/json-schema-document.unit.test.ts | 11 ++++++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/config/src/json-schema-document.ts b/packages/config/src/json-schema-document.ts index d524f84427..9fe55f4f53 100644 --- a/packages/config/src/json-schema-document.ts +++ b/packages/config/src/json-schema-document.ts @@ -96,12 +96,9 @@ function envReferenceBranch(): Node { } function isEnvReferenceBranch(node: unknown): boolean { - return ( - isNode(node) && - Object.keys(node).length === 2 && - node["type"] === "string" && - node["pattern"] === ENV_PATTERN - ); + 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 { diff --git a/packages/config/src/json-schema-document.unit.test.ts b/packages/config/src/json-schema-document.unit.test.ts index 2773d739c8..1e0cb4c19c 100644 --- a/packages/config/src/json-schema-document.unit.test.ts +++ b/packages/config/src/json-schema-document.unit.test.ts @@ -577,8 +577,17 @@ describe("toPublishedJsonSchemaDocument acceptEnvReferences", () => { }); 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()]) }), true); + 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", () => {