From fc38a1202ed560184995fb16a48a0fe56ec26251 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 14:52:40 +0200 Subject: [PATCH 01/10] test(cli): codify local stack config parity --- .../next/config/local-stack-config-parity.ts | 523 ++++++++++++++++++ .../local-stack-config-parity.unit.test.ts | 77 +++ 2 files changed, 600 insertions(+) create mode 100644 apps/cli/src/next/config/local-stack-config-parity.ts create mode 100644 apps/cli/src/next/config/local-stack-config-parity.unit.test.ts diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts new file mode 100644 index 0000000000..57a2abdf6a --- /dev/null +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -0,0 +1,523 @@ +import type { ProjectConfig } from "@supabase/config"; + +/** + * The disposition of one project-config leaf in the next local-stack flow. + * + * `presence` tells the future launch resolver whether the decoded value is + * sufficient or whether it must also inspect the loaded source document. Most + * schema defaults erase the distinction between an omitted field and an + * explicitly configured default value, which matters when unsupported fields + * must be rejected or warned about without rejecting untouched defaults. + */ +type LocalStackConfigParityDecision = + | { + readonly _tag: "mapped"; + readonly presence: "decoded-value" | "raw-document"; + readonly mappedBy: "start" | "functions-dev" | "stack-functions-runtime"; + readonly rationale: string; + } + | { + readonly _tag: "not-applicable"; + readonly presence: "decoded-value" | "raw-document"; + readonly rationale: string; + } + | { + readonly _tag: "unsupported-blocking"; + readonly presence: "decoded-value" | "raw-document"; + readonly rationale: string; + } + | { + readonly _tag: "unsupported-warning"; + readonly presence: "decoded-value" | "raw-document"; + readonly rationale: string; + }; + +export interface LocalStackConfigParitySection { + readonly [field: string]: LocalStackConfigParityDecision | LocalStackConfigParitySection; +} + +type Node = LocalStackConfigParityDecision | LocalStackConfigParitySection; + +const unsupportedRuntimeField: LocalStackConfigParityDecision = { + _tag: "unsupported-blocking", + presence: "raw-document", + rationale: + "An explicit value changes local runtime behavior but the next stack launch Adapter does not translate it yet.", +}; + +const unsupportedOptionalRuntimeField: LocalStackConfigParityDecision = { + _tag: "unsupported-blocking", + presence: "decoded-value", + rationale: + "An explicitly present optional value changes local runtime behavior but the next stack launch Adapter does not translate it yet.", +}; + +const unsupportedSecretRuntimeField: LocalStackConfigParityDecision = { + _tag: "unsupported-blocking", + presence: "decoded-value", + rationale: + "An explicitly configured secret changes local runtime credentials but the next stack launch Adapter does not translate it yet.", +}; + +const mappedAutoExposeNewTables: LocalStackConfigParityDecision = { + _tag: "mapped", + presence: "raw-document", + mappedBy: "start", + rationale: + "The start command resolves the tri-state value, emits its deprecation warning, and passes it to PostgreSQL initialization.", +}; + +const mappedFunctionManifest: LocalStackConfigParityDecision = { + _tag: "mapped", + presence: "raw-document", + mappedBy: "stack-functions-runtime", + rationale: + "The current stack functions runtime resolves every configured function entry, including enablement, JWT verification, paths, static files, and environment values.", +}; + +const functionConfigParity = { + enabled: mappedFunctionManifest, + verify_jwt: mappedFunctionManifest, + import_map: mappedFunctionManifest, + entrypoint: mappedFunctionManifest, + static_files: mappedFunctionManifest, + env: mappedFunctionManifest, +} satisfies Record; + +const mappedFunctionsDevEdgeRuntime: LocalStackConfigParityDecision = { + _tag: "mapped", + presence: "raw-document", + mappedBy: "functions-dev", + rationale: + "The functions-dev Adapter resolves this field and passes it to the stack edge-runtime configuration.", +}; + +const commandOnlyDatabaseField: LocalStackConfigParityDecision = { + _tag: "not-applicable", + presence: "raw-document", + rationale: + "This field configures database tooling outside local stack startup and does not belong in StackConfig.", +}; + +const remoteOverlayField: LocalStackConfigParityDecision = { + _tag: "not-applicable", + presence: "raw-document", + rationale: + "Remote overlays are selected and merged by project configuration resolution before the local stack launch Adapter runs.", +}; + +const unsupportedFutureRuntimeField: LocalStackConfigParityDecision = { + _tag: "unsupported-warning", + presence: "raw-document", + rationale: + "This experimental field has no stable local stack contract yet; an explicit value must be surfaced rather than silently ignored.", +}; + +const authExternalProviderParity = { + enabled: unsupportedRuntimeField, + client_id: unsupportedRuntimeField, + secret: unsupportedSecretRuntimeField, + url: unsupportedRuntimeField, + redirect_uri: unsupportedRuntimeField, + skip_nonce_check: unsupportedRuntimeField, + email_optional: unsupportedRuntimeField, +} satisfies Record; + +const authHookParity = { + enabled: unsupportedRuntimeField, + uri: unsupportedOptionalRuntimeField, + secrets: unsupportedSecretRuntimeField, +} satisfies Record; + +const authRateLimitParity = { + email_sent: unsupportedRuntimeField, + sms_sent: unsupportedRuntimeField, + anonymous_users: unsupportedRuntimeField, + token_refresh: unsupportedRuntimeField, + sign_in_sign_ups: unsupportedRuntimeField, + token_verifications: unsupportedRuntimeField, + web3: unsupportedRuntimeField, +} satisfies Record; + +const authExternalParity = { + apple: authExternalProviderParity, + azure: authExternalProviderParity, + bitbucket: authExternalProviderParity, + discord: authExternalProviderParity, + facebook: authExternalProviderParity, + github: authExternalProviderParity, + gitlab: authExternalProviderParity, + google: authExternalProviderParity, + kakao: authExternalProviderParity, + keycloak: authExternalProviderParity, + linkedin_oidc: authExternalProviderParity, + notion: authExternalProviderParity, + twitch: authExternalProviderParity, + twitter: authExternalProviderParity, + x: authExternalProviderParity, + slack_oidc: authExternalProviderParity, + spotify: authExternalProviderParity, + workos: authExternalProviderParity, + zoom: authExternalProviderParity, +} satisfies Record; + +const authHooksParity = { + mfa_verification_attempt: authHookParity, + password_verification_attempt: authHookParity, + custom_access_token: authHookParity, + send_sms: authHookParity, + send_email: authHookParity, + before_user_created: authHookParity, +} satisfies Record; + +const authSmsParity = { + enable_signup: unsupportedRuntimeField, + enable_confirmations: unsupportedRuntimeField, + template: unsupportedRuntimeField, + max_frequency: unsupportedRuntimeField, + twilio: { + enabled: unsupportedRuntimeField, + account_sid: unsupportedRuntimeField, + message_service_sid: unsupportedRuntimeField, + auth_token: unsupportedSecretRuntimeField, + } satisfies Record, + twilio_verify: { + enabled: unsupportedRuntimeField, + account_sid: unsupportedOptionalRuntimeField, + message_service_sid: unsupportedOptionalRuntimeField, + auth_token: unsupportedSecretRuntimeField, + } satisfies Record, + messagebird: { + enabled: unsupportedRuntimeField, + originator: unsupportedOptionalRuntimeField, + access_key: unsupportedSecretRuntimeField, + } satisfies Record, + textlocal: { + enabled: unsupportedRuntimeField, + sender: unsupportedOptionalRuntimeField, + api_key: unsupportedSecretRuntimeField, + } satisfies Record, + vonage: { + enabled: unsupportedRuntimeField, + from: unsupportedOptionalRuntimeField, + api_key: unsupportedOptionalRuntimeField, + api_secret: unsupportedSecretRuntimeField, + } satisfies Record, + test_otp: unsupportedOptionalRuntimeField, +} satisfies Record; + +const authParity = { + enabled: unsupportedRuntimeField, + site_url: unsupportedRuntimeField, + additional_redirect_urls: unsupportedRuntimeField, + jwt_expiry: unsupportedRuntimeField, + jwt_issuer: unsupportedOptionalRuntimeField, + signing_keys_path: unsupportedOptionalRuntimeField, + enable_refresh_token_rotation: unsupportedRuntimeField, + refresh_token_reuse_interval: unsupportedRuntimeField, + enable_manual_linking: unsupportedRuntimeField, + enable_signup: unsupportedRuntimeField, + enable_anonymous_sign_ins: unsupportedRuntimeField, + minimum_password_length: unsupportedRuntimeField, + password_requirements: unsupportedRuntimeField, + publishable_key: unsupportedSecretRuntimeField, + secret_key: unsupportedSecretRuntimeField, + jwt_secret: unsupportedSecretRuntimeField, + anon_key: unsupportedSecretRuntimeField, + service_role_key: unsupportedSecretRuntimeField, + rate_limit: authRateLimitParity, + captcha: { + enabled: unsupportedRuntimeField, + provider: unsupportedOptionalRuntimeField, + secret: unsupportedSecretRuntimeField, + } satisfies Record, Node>, + hook: authHooksParity, + mfa: { + totp: { + enroll_enabled: unsupportedRuntimeField, + verify_enabled: unsupportedRuntimeField, + } satisfies Record, + phone: { + enroll_enabled: unsupportedRuntimeField, + verify_enabled: unsupportedRuntimeField, + otp_length: unsupportedRuntimeField, + template: unsupportedRuntimeField, + max_frequency: unsupportedRuntimeField, + } satisfies Record, + web_authn: { + enroll_enabled: unsupportedRuntimeField, + verify_enabled: unsupportedRuntimeField, + } satisfies Record, + max_enrolled_factors: unsupportedRuntimeField, + } satisfies Record, + sessions: { + timebox: unsupportedOptionalRuntimeField, + inactivity_timeout: unsupportedOptionalRuntimeField, + } satisfies Record, Node>, + email: { + enable_signup: unsupportedRuntimeField, + double_confirm_changes: unsupportedRuntimeField, + enable_confirmations: unsupportedRuntimeField, + secure_password_change: unsupportedRuntimeField, + max_frequency: unsupportedRuntimeField, + otp_length: unsupportedRuntimeField, + otp_expiry: unsupportedRuntimeField, + smtp: { + enabled: unsupportedRuntimeField, + host: unsupportedOptionalRuntimeField, + port: unsupportedOptionalRuntimeField, + user: unsupportedOptionalRuntimeField, + pass: unsupportedSecretRuntimeField, + admin_email: unsupportedOptionalRuntimeField, + sender_name: unsupportedOptionalRuntimeField, + } satisfies Record, Node>, + template: { + "*": { + subject: unsupportedRuntimeField, + content_path: unsupportedRuntimeField, + } satisfies Record, + }, + notification: { + "*": { + enabled: unsupportedRuntimeField, + subject: unsupportedRuntimeField, + content_path: unsupportedRuntimeField, + } satisfies Record, + }, + } satisfies Record, + sms: authSmsParity, + external: authExternalParity, + web3: { + solana: { + enabled: unsupportedRuntimeField, + } satisfies Record, + ethereum: { + enabled: unsupportedRuntimeField, + } satisfies Record, + } satisfies Record, + oauth_server: { + enabled: unsupportedRuntimeField, + authorization_url_path: unsupportedRuntimeField, + allow_dynamic_registration: unsupportedRuntimeField, + } satisfies Record, + third_party: { + firebase: { + enabled: unsupportedRuntimeField, + project_id: unsupportedOptionalRuntimeField, + } satisfies Record, + auth0: { + enabled: unsupportedRuntimeField, + tenant: unsupportedOptionalRuntimeField, + tenant_region: unsupportedOptionalRuntimeField, + } satisfies Record, + aws_cognito: { + enabled: unsupportedRuntimeField, + user_pool_id: unsupportedOptionalRuntimeField, + user_pool_region: unsupportedOptionalRuntimeField, + } satisfies Record, + clerk: { + enabled: unsupportedRuntimeField, + domain: unsupportedOptionalRuntimeField, + } satisfies Record, + workos: { + enabled: unsupportedRuntimeField, + issuer_url: unsupportedOptionalRuntimeField, + } satisfies Record, + } satisfies Record, +} satisfies Record; + +const dbSettingsParity = { + effective_cache_size: unsupportedOptionalRuntimeField, + logical_decoding_work_mem: unsupportedOptionalRuntimeField, + maintenance_work_mem: unsupportedOptionalRuntimeField, + max_connections: unsupportedOptionalRuntimeField, + max_locks_per_transaction: unsupportedOptionalRuntimeField, + max_parallel_maintenance_workers: unsupportedOptionalRuntimeField, + max_parallel_workers: unsupportedOptionalRuntimeField, + max_parallel_workers_per_gather: unsupportedOptionalRuntimeField, + max_replication_slots: unsupportedOptionalRuntimeField, + max_slot_wal_keep_size: unsupportedOptionalRuntimeField, + max_standby_archive_delay: unsupportedOptionalRuntimeField, + max_standby_streaming_delay: unsupportedOptionalRuntimeField, + max_wal_size: unsupportedOptionalRuntimeField, + max_wal_senders: unsupportedOptionalRuntimeField, + max_worker_processes: unsupportedOptionalRuntimeField, + session_replication_role: unsupportedOptionalRuntimeField, + shared_buffers: unsupportedOptionalRuntimeField, + statement_timeout: unsupportedOptionalRuntimeField, + track_activity_query_size: unsupportedOptionalRuntimeField, + track_commit_timestamp: unsupportedOptionalRuntimeField, + wal_keep_size: unsupportedOptionalRuntimeField, + wal_sender_timeout: unsupportedOptionalRuntimeField, + work_mem: unsupportedOptionalRuntimeField, +} satisfies Record, Node>; + +/** + * Executable inventory for the current next local-stack implementation. + * + * Every fixed project-config object is checked against its schema-derived + * `keyof` type. Adding or removing a field in `@supabase/config` therefore + * requires an explicit parity decision here before the CLI type-check passes. + * Dynamic records use `*` for user-provided keys while their fixed value shape + * is checked exhaustively. Scalar-valued records such as vault and secrets are + * classified at the record field itself. + */ +const localStackConfigParity = { + project_id: unsupportedOptionalRuntimeField, + analytics: { + enabled: unsupportedRuntimeField, + port: unsupportedRuntimeField, + backend: unsupportedRuntimeField, + vector_port: unsupportedOptionalRuntimeField, + gcp_project_id: unsupportedOptionalRuntimeField, + gcp_project_number: unsupportedOptionalRuntimeField, + gcp_jwt_path: unsupportedOptionalRuntimeField, + } satisfies Record, + api: { + enabled: unsupportedRuntimeField, + port: unsupportedRuntimeField, + schemas: unsupportedRuntimeField, + extra_search_path: unsupportedRuntimeField, + max_rows: unsupportedRuntimeField, + auto_expose_new_tables: mappedAutoExposeNewTables, + tls: { + enabled: unsupportedRuntimeField, + cert_path: unsupportedOptionalRuntimeField, + key_path: unsupportedOptionalRuntimeField, + } satisfies Record, + external_url: unsupportedOptionalRuntimeField, + } satisfies Record, + auth: authParity, + db: { + port: unsupportedRuntimeField, + shadow_port: commandOnlyDatabaseField, + health_timeout: unsupportedRuntimeField, + major_version: unsupportedRuntimeField, + pooler: { + enabled: unsupportedRuntimeField, + port: unsupportedRuntimeField, + pool_mode: unsupportedRuntimeField, + default_pool_size: unsupportedRuntimeField, + max_client_conn: unsupportedRuntimeField, + } satisfies Record, + migrations: { + enabled: unsupportedRuntimeField, + schema_paths: unsupportedRuntimeField, + } satisfies Record, + seed: { + enabled: unsupportedRuntimeField, + sql_paths: unsupportedRuntimeField, + } satisfies Record, + settings: dbSettingsParity, + network_restrictions: { + enabled: commandOnlyDatabaseField, + allowed_cidrs: commandOnlyDatabaseField, + allowed_cidrs_v6: commandOnlyDatabaseField, + } satisfies Record, + ssl_enforcement: { + enabled: unsupportedRuntimeField, + } satisfies Record, Node>, + vault: unsupportedSecretRuntimeField, + } satisfies Record, + edge_runtime: { + enabled: mappedFunctionsDevEdgeRuntime, + policy: mappedFunctionsDevEdgeRuntime, + inspector_port: mappedFunctionsDevEdgeRuntime, + deno_version: unsupportedRuntimeField, + secrets: mappedFunctionsDevEdgeRuntime, + } satisfies Record, + functions: { + "*": functionConfigParity, + }, + local_smtp: { + enabled: unsupportedRuntimeField, + port: unsupportedRuntimeField, + smtp_port: unsupportedOptionalRuntimeField, + pop3_port: unsupportedOptionalRuntimeField, + admin_email: unsupportedOptionalRuntimeField, + sender_name: unsupportedOptionalRuntimeField, + } satisfies Record, + realtime: { + enabled: unsupportedRuntimeField, + ip_version: unsupportedRuntimeField, + max_header_length: unsupportedRuntimeField, + } satisfies Record, + storage: { + enabled: unsupportedRuntimeField, + file_size_limit: unsupportedRuntimeField, + image_transformation: { + enabled: unsupportedRuntimeField, + } satisfies Record, Node>, + buckets: { + "*": { + public: unsupportedRuntimeField, + file_size_limit: unsupportedRuntimeField, + allowed_mime_types: unsupportedRuntimeField, + objects_path: unsupportedRuntimeField, + } satisfies Record[string], Node>, + }, + s3_protocol: { + enabled: unsupportedRuntimeField, + } satisfies Record, + analytics: { + enabled: unsupportedRuntimeField, + max_namespaces: unsupportedRuntimeField, + max_tables: unsupportedRuntimeField, + max_catalogs: unsupportedRuntimeField, + buckets: unsupportedRuntimeField, + } satisfies Record, + vector: { + enabled: unsupportedRuntimeField, + max_buckets: unsupportedRuntimeField, + max_indexes: unsupportedRuntimeField, + buckets: unsupportedRuntimeField, + } satisfies Record, + } satisfies Record, + studio: { + enabled: unsupportedRuntimeField, + port: unsupportedRuntimeField, + api_url: unsupportedRuntimeField, + openai_api_key: unsupportedSecretRuntimeField, + } satisfies Record, + experimental: { + orioledb_version: unsupportedFutureRuntimeField, + s3_host: unsupportedFutureRuntimeField, + s3_region: unsupportedFutureRuntimeField, + s3_access_key: unsupportedFutureRuntimeField, + s3_secret_key: unsupportedFutureRuntimeField, + webhooks: { + enabled: unsupportedFutureRuntimeField, + } satisfies Record, Node>, + pgdelta: { + enabled: commandOnlyDatabaseField, + declarative_schema_path: commandOnlyDatabaseField, + format_options: commandOnlyDatabaseField, + } satisfies Record, Node>, + inspect: { + rules: commandOnlyDatabaseField, + } satisfies Record, Node>, + } satisfies Record, + remotes: remoteOverlayField, +} satisfies Record; + +export interface LocalStackConfigParityEntry { + readonly path: string; + readonly decision: LocalStackConfigParityDecision; +} + +function isDecision(node: Node): node is LocalStackConfigParityDecision { + return "_tag" in node; +} + +/** Flattens the nested, compile-checked ledger for diagnostics and tests. */ +export function flattenLocalStackConfigParity( + section: LocalStackConfigParitySection = localStackConfigParity, + prefix = "", +): ReadonlyArray { + return Object.entries(section).flatMap(([field, node]) => { + const path = prefix === "" ? field : `${prefix}.${field}`; + return isDecision(node) + ? [{ path, decision: node }] + : flattenLocalStackConfigParity(node, path); + }); +} diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts new file mode 100644 index 0000000000..69a75e96a4 --- /dev/null +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { flattenLocalStackConfigParity } from "./local-stack-config-parity.ts"; + +describe("localStackConfigParity", () => { + const entries = flattenLocalStackConfigParity(); + + it("classifies every fixed project-config leaf exactly once", () => { + const paths = entries.map(({ path }) => path); + + expect(paths).toHaveLength(361); + expect(new Set(paths).size).toBe(paths.length); + expect( + Object.fromEntries( + ["mapped", "not-applicable", "unsupported-blocking", "unsupported-warning"].map((tag) => [ + tag, + entries.filter(({ decision }) => decision._tag === tag).length, + ]), + ), + ).toEqual({ + mapped: 11, + "not-applicable": 9, + "unsupported-blocking": 335, + "unsupported-warning": 6, + }); + }); + + it("claims only behavior implemented by a current next local-runtime flow as mapped", () => { + expect( + entries + .filter(({ decision }) => decision._tag === "mapped") + .map(({ path }) => path) + .sort(), + ).toEqual([ + "api.auto_expose_new_tables", + "edge_runtime.enabled", + "edge_runtime.inspector_port", + "edge_runtime.policy", + "edge_runtime.secrets", + "functions.*.enabled", + "functions.*.entrypoint", + "functions.*.env", + "functions.*.import_map", + "functions.*.static_files", + "functions.*.verify_jwt", + ]); + }); + + it("preserves raw-document requirements for presence-sensitive sections", () => { + const byPath = new Map(entries.map(({ path, decision }) => [path, decision])); + + expect(byPath.get("api.auto_expose_new_tables")?.presence).toBe("raw-document"); + expect(byPath.get("auth.external.github.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("auth.hook.send_email.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("auth.sms.twilio.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("storage.image_transformation.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("experimental.webhooks.enabled")?.presence).toBe("raw-document"); + }); + + it("keeps non-runtime project configuration out of StackConfig", () => { + expect( + entries + .filter(({ decision }) => decision._tag === "not-applicable") + .map(({ path }) => path) + .sort(), + ).toEqual([ + "db.network_restrictions.allowed_cidrs", + "db.network_restrictions.allowed_cidrs_v6", + "db.network_restrictions.enabled", + "db.shadow_port", + "experimental.inspect.rules", + "experimental.pgdelta.declarative_schema_path", + "experimental.pgdelta.enabled", + "experimental.pgdelta.format_options", + "remotes", + ]); + }); +}); From 3e70eb6033a98e37a286eb2e7d4847c7ff75db2a Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 17:00:36 +0200 Subject: [PATCH 02/10] test(cli): refine config parity decisions --- .../next/config/local-stack-config-parity.ts | 76 ++++++++++++++----- .../local-stack-config-parity.unit.test.ts | 30 +++++++- 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts index 57a2abdf6a..7d03314a26 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -33,10 +33,18 @@ type LocalStackConfigParityDecision = }; export interface LocalStackConfigParitySection { - readonly [field: string]: LocalStackConfigParityDecision | LocalStackConfigParitySection; + readonly [field: string]: Node; } -type Node = LocalStackConfigParityDecision | LocalStackConfigParitySection; +interface LocalStackConfigParityBranch { + readonly decision: LocalStackConfigParityDecision; + readonly children: LocalStackConfigParitySection; +} + +type Node = + | LocalStackConfigParityDecision + | LocalStackConfigParityBranch + | LocalStackConfigParitySection; const unsupportedRuntimeField: LocalStackConfigParityDecision = { _tag: "unsupported-blocking", @@ -99,6 +107,13 @@ const commandOnlyDatabaseField: LocalStackConfigParityDecision = { "This field configures database tooling outside local stack startup and does not belong in StackConfig.", }; +const projectMetadataField: LocalStackConfigParityDecision = { + _tag: "not-applicable", + presence: "raw-document", + rationale: + "The project identifier distinguishes local project directories and is not local stack runtime configuration.", +}; + const remoteOverlayField: LocalStackConfigParityDecision = { _tag: "not-applicable", presence: "raw-document", @@ -273,16 +288,22 @@ const authParity = { } satisfies Record, Node>, template: { "*": { - subject: unsupportedRuntimeField, - content_path: unsupportedRuntimeField, - } satisfies Record, + decision: unsupportedRuntimeField, + children: { + subject: unsupportedRuntimeField, + content_path: unsupportedRuntimeField, + } satisfies Record, + }, }, notification: { "*": { - enabled: unsupportedRuntimeField, - subject: unsupportedRuntimeField, - content_path: unsupportedRuntimeField, - } satisfies Record, + decision: unsupportedRuntimeField, + children: { + enabled: unsupportedRuntimeField, + subject: unsupportedRuntimeField, + content_path: unsupportedRuntimeField, + } satisfies Record, + }, }, } satisfies Record, sms: authSmsParity, @@ -363,7 +384,7 @@ const dbSettingsParity = { * classified at the record field itself. */ const localStackConfigParity = { - project_id: unsupportedOptionalRuntimeField, + project_id: projectMetadataField, analytics: { enabled: unsupportedRuntimeField, port: unsupportedRuntimeField, @@ -427,7 +448,10 @@ const localStackConfigParity = { secrets: mappedFunctionsDevEdgeRuntime, } satisfies Record, functions: { - "*": functionConfigParity, + "*": { + decision: mappedFunctionManifest, + children: functionConfigParity, + }, }, local_smtp: { enabled: unsupportedRuntimeField, @@ -450,11 +474,14 @@ const localStackConfigParity = { } satisfies Record, Node>, buckets: { "*": { - public: unsupportedRuntimeField, - file_size_limit: unsupportedRuntimeField, - allowed_mime_types: unsupportedRuntimeField, - objects_path: unsupportedRuntimeField, - } satisfies Record[string], Node>, + decision: unsupportedRuntimeField, + children: { + public: unsupportedRuntimeField, + file_size_limit: unsupportedRuntimeField, + allowed_mime_types: unsupportedRuntimeField, + objects_path: unsupportedRuntimeField, + } satisfies Record[string], Node>, + }, }, s3_protocol: { enabled: unsupportedRuntimeField, @@ -509,6 +536,10 @@ function isDecision(node: Node): node is LocalStackConfigParityDecision { return "_tag" in node; } +function isBranch(node: Node): node is LocalStackConfigParityBranch { + return "decision" in node && "children" in node; +} + /** Flattens the nested, compile-checked ledger for diagnostics and tests. */ export function flattenLocalStackConfigParity( section: LocalStackConfigParitySection = localStackConfigParity, @@ -516,8 +547,15 @@ export function flattenLocalStackConfigParity( ): ReadonlyArray { return Object.entries(section).flatMap(([field, node]) => { const path = prefix === "" ? field : `${prefix}.${field}`; - return isDecision(node) - ? [{ path, decision: node }] - : flattenLocalStackConfigParity(node, path); + if (isDecision(node)) { + return [{ path, decision: node }]; + } + if (isBranch(node)) { + return [ + { path, decision: node.decision }, + ...flattenLocalStackConfigParity(node.children, path), + ]; + } + return flattenLocalStackConfigParity(node, path); }); } diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts index 69a75e96a4..9cc94bb536 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -7,7 +7,7 @@ describe("localStackConfigParity", () => { it("classifies every fixed project-config leaf exactly once", () => { const paths = entries.map(({ path }) => path); - expect(paths).toHaveLength(361); + expect(paths).toHaveLength(365); expect(new Set(paths).size).toBe(paths.length); expect( Object.fromEntries( @@ -17,9 +17,9 @@ describe("localStackConfigParity", () => { ]), ), ).toEqual({ - mapped: 11, - "not-applicable": 9, - "unsupported-blocking": 335, + mapped: 12, + "not-applicable": 10, + "unsupported-blocking": 337, "unsupported-warning": 6, }); }); @@ -36,6 +36,7 @@ describe("localStackConfigParity", () => { "edge_runtime.inspector_port", "edge_runtime.policy", "edge_runtime.secrets", + "functions.*", "functions.*.enabled", "functions.*.entrypoint", "functions.*.env", @@ -43,6 +44,25 @@ describe("localStackConfigParity", () => { "functions.*.static_files", "functions.*.verify_jwt", ]); + + expect( + entries.flatMap(({ path, decision }) => + decision._tag === "mapped" ? [`${decision.mappedBy}:${path}`] : [], + ), + ).toEqual([ + "start:api.auto_expose_new_tables", + "functions-dev:edge_runtime.enabled", + "functions-dev:edge_runtime.policy", + "functions-dev:edge_runtime.inspector_port", + "functions-dev:edge_runtime.secrets", + "stack-functions-runtime:functions.*", + "stack-functions-runtime:functions.*.enabled", + "stack-functions-runtime:functions.*.verify_jwt", + "stack-functions-runtime:functions.*.import_map", + "stack-functions-runtime:functions.*.entrypoint", + "stack-functions-runtime:functions.*.static_files", + "stack-functions-runtime:functions.*.env", + ]); }); it("preserves raw-document requirements for presence-sensitive sections", () => { @@ -53,6 +73,7 @@ describe("localStackConfigParity", () => { expect(byPath.get("auth.hook.send_email.enabled")?.presence).toBe("raw-document"); expect(byPath.get("auth.sms.twilio.enabled")?.presence).toBe("raw-document"); expect(byPath.get("storage.image_transformation.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("storage.buckets.*")?.presence).toBe("raw-document"); expect(byPath.get("experimental.webhooks.enabled")?.presence).toBe("raw-document"); }); @@ -71,6 +92,7 @@ describe("localStackConfigParity", () => { "experimental.pgdelta.declarative_schema_path", "experimental.pgdelta.enabled", "experimental.pgdelta.format_options", + "project_id", "remotes", ]); }); From c5b915280a126a398825d78dae613d3dc5683ab1 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 17:19:22 +0200 Subject: [PATCH 03/10] test(stack): await stopped lifecycle state --- packages/stack/src/Stack.unit.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/stack/src/Stack.unit.test.ts b/packages/stack/src/Stack.unit.test.ts index 5f40c98cee..4470c7fc85 100644 --- a/packages/stack/src/Stack.unit.test.ts +++ b/packages/stack/src/Stack.unit.test.ts @@ -745,8 +745,14 @@ describe("Stack", () => { return Effect.gen(function* () { const coordinator = yield* StackLifecycleCoordinator; yield* coordinator.start(); + const stateChanges = yield* coordinator.stateChanges("auth"); + const stoppedState = yield* stateChanges.pipe( + Stream.filter((state) => state.status === "Stopped"), + Stream.runHead, + Effect.forkChild({ startImmediately: true }), + ); yield* coordinator.stopService("auth"); - yield* Effect.sleep("20 millis"); + yield* Fiber.join(stoppedState); expect((yield* coordinator.getState("auth")).status).toBe("Stopped"); yield* coordinator.stop(); From 73392f3c0b7cad72b85ec46eda3cc63a46107305 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 17:33:50 +0200 Subject: [PATCH 04/10] test(cli): tighten parity classifications --- .../next/config/local-stack-config-parity.ts | 44 +++++++++++++------ .../local-stack-config-parity.unit.test.ts | 8 ++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts index 7d03314a26..d45b6f8342 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -3,32 +3,34 @@ import type { ProjectConfig } from "@supabase/config"; /** * The disposition of one project-config leaf in the next local-stack flow. * - * `presence` tells the future launch resolver whether the decoded value is - * sufficient or whether it must also inspect the loaded source document. Most - * schema defaults erase the distinction between an omitted field and an - * explicitly configured default value, which matters when unsupported fields - * must be rejected or warned about without rejecting untouched defaults. + * `presence` tells the future launch resolver how to determine whether a field + * affects the local runtime. Most schema defaults erase the distinction between + * an omitted field and an explicitly configured default value. Secrets need an + * additional check: generated `env(...)` placeholders that did not resolve and + * secrets inside disabled subtrees do not affect the runtime. */ +type LocalStackConfigParityPresence = "decoded-value" | "effective-secret" | "raw-document"; + type LocalStackConfigParityDecision = | { readonly _tag: "mapped"; - readonly presence: "decoded-value" | "raw-document"; + readonly presence: LocalStackConfigParityPresence; readonly mappedBy: "start" | "functions-dev" | "stack-functions-runtime"; readonly rationale: string; } | { readonly _tag: "not-applicable"; - readonly presence: "decoded-value" | "raw-document"; + readonly presence: LocalStackConfigParityPresence; readonly rationale: string; } | { readonly _tag: "unsupported-blocking"; - readonly presence: "decoded-value" | "raw-document"; + readonly presence: LocalStackConfigParityPresence; readonly rationale: string; } | { readonly _tag: "unsupported-warning"; - readonly presence: "decoded-value" | "raw-document"; + readonly presence: LocalStackConfigParityPresence; readonly rationale: string; }; @@ -62,9 +64,9 @@ const unsupportedOptionalRuntimeField: LocalStackConfigParityDecision = { const unsupportedSecretRuntimeField: LocalStackConfigParityDecision = { _tag: "unsupported-blocking", - presence: "decoded-value", + presence: "effective-secret", rationale: - "An explicitly configured secret changes local runtime credentials but the next stack launch Adapter does not translate it yet.", + "A concrete resolved secret in an enabled runtime subtree changes local credentials but the next stack launch Adapter does not translate it yet; unresolved generated env placeholders do not count.", }; const mappedAutoExposeNewTables: LocalStackConfigParityDecision = { @@ -80,7 +82,14 @@ const mappedFunctionManifest: LocalStackConfigParityDecision = { presence: "raw-document", mappedBy: "stack-functions-runtime", rationale: - "The current stack functions runtime resolves every configured function entry, including enablement, JWT verification, paths, static files, and environment values.", + "The current stack functions runtime resolves every configured function entry, including enablement, JWT verification, paths, and static files.", +}; + +const unsupportedPerFunctionEnv: LocalStackConfigParityDecision = { + _tag: "unsupported-blocking", + presence: "raw-document", + rationale: + "The current stack functions runtime merges every function environment into one global record, so per-function overrides are not preserved.", }; const functionConfigParity = { @@ -89,7 +98,7 @@ const functionConfigParity = { import_map: mappedFunctionManifest, entrypoint: mappedFunctionManifest, static_files: mappedFunctionManifest, - env: mappedFunctionManifest, + env: unsupportedPerFunctionEnv, } satisfies Record; const mappedFunctionsDevEdgeRuntime: LocalStackConfigParityDecision = { @@ -138,6 +147,13 @@ const authExternalProviderParity = { email_optional: unsupportedRuntimeField, } satisfies Record; +type AuthExternalParity = { + readonly [Provider in keyof ProjectConfig["auth"]["external"]]: Record< + keyof ProjectConfig["auth"]["external"][Provider], + Node + >; +}; + const authHookParity = { enabled: unsupportedRuntimeField, uri: unsupportedOptionalRuntimeField, @@ -174,7 +190,7 @@ const authExternalParity = { spotify: authExternalProviderParity, workos: authExternalProviderParity, zoom: authExternalProviderParity, -} satisfies Record; +} satisfies AuthExternalParity; const authHooksParity = { mfa_verification_attempt: authHookParity, diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts index 9cc94bb536..b94884bcb0 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -17,9 +17,9 @@ describe("localStackConfigParity", () => { ]), ), ).toEqual({ - mapped: 12, + mapped: 11, "not-applicable": 10, - "unsupported-blocking": 337, + "unsupported-blocking": 338, "unsupported-warning": 6, }); }); @@ -39,7 +39,6 @@ describe("localStackConfigParity", () => { "functions.*", "functions.*.enabled", "functions.*.entrypoint", - "functions.*.env", "functions.*.import_map", "functions.*.static_files", "functions.*.verify_jwt", @@ -61,7 +60,6 @@ describe("localStackConfigParity", () => { "stack-functions-runtime:functions.*.import_map", "stack-functions-runtime:functions.*.entrypoint", "stack-functions-runtime:functions.*.static_files", - "stack-functions-runtime:functions.*.env", ]); }); @@ -75,6 +73,8 @@ describe("localStackConfigParity", () => { expect(byPath.get("storage.image_transformation.enabled")?.presence).toBe("raw-document"); expect(byPath.get("storage.buckets.*")?.presence).toBe("raw-document"); expect(byPath.get("experimental.webhooks.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("auth.external.apple.secret")?.presence).toBe("effective-secret"); + expect(byPath.get("studio.openai_api_key")?.presence).toBe("effective-secret"); }); it("keeps non-runtime project configuration out of StackConfig", () => { From e6d8e8b20287d7da5f27543b6220a8818a8f411b Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 17:59:40 +0200 Subject: [PATCH 05/10] test(cli): refine parity presence rules --- .../next/config/local-stack-config-parity.ts | 51 ++++++++++++++----- .../local-stack-config-parity.unit.test.ts | 7 ++- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts index d45b6f8342..af5daceec4 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -5,11 +5,15 @@ import type { ProjectConfig } from "@supabase/config"; * * `presence` tells the future launch resolver how to determine whether a field * affects the local runtime. Most schema defaults erase the distinction between - * an omitted field and an explicitly configured default value. Secrets need an - * additional check: generated `env(...)` placeholders that did not resolve and - * secrets inside disabled subtrees do not affect the runtime. + * an omitted field and an explicitly configured default value. Generated + * disabled provider stubs and secrets inside disabled subtrees do not affect the + * runtime; unresolved `env(...)` placeholders do not provide concrete secrets. */ -type LocalStackConfigParityPresence = "decoded-value" | "effective-secret" | "raw-document"; +type LocalStackConfigParityPresence = + | "decoded-value" + | "effective-secret" + | "enabled-subtree" + | "raw-document"; type LocalStackConfigParityDecision = | { @@ -69,6 +73,13 @@ const unsupportedSecretRuntimeField: LocalStackConfigParityDecision = { "A concrete resolved secret in an enabled runtime subtree changes local credentials but the next stack launch Adapter does not translate it yet; unresolved generated env placeholders do not count.", }; +const unsupportedEnabledProviderField: LocalStackConfigParityDecision = { + _tag: "unsupported-blocking", + presence: "enabled-subtree", + rationale: + "This setting changes local authentication behavior only when its provider is effectively enabled; generated disabled provider stubs do not count.", +}; + const mappedAutoExposeNewTables: LocalStackConfigParityDecision = { _tag: "mapped", presence: "raw-document", @@ -138,13 +149,13 @@ const unsupportedFutureRuntimeField: LocalStackConfigParityDecision = { }; const authExternalProviderParity = { - enabled: unsupportedRuntimeField, - client_id: unsupportedRuntimeField, + enabled: unsupportedEnabledProviderField, + client_id: unsupportedEnabledProviderField, secret: unsupportedSecretRuntimeField, - url: unsupportedRuntimeField, - redirect_uri: unsupportedRuntimeField, - skip_nonce_check: unsupportedRuntimeField, - email_optional: unsupportedRuntimeField, + url: unsupportedEnabledProviderField, + redirect_uri: unsupportedEnabledProviderField, + skip_nonce_check: unsupportedEnabledProviderField, + email_optional: unsupportedEnabledProviderField, } satisfies Record; type AuthExternalParity = { @@ -507,13 +518,29 @@ const localStackConfigParity = { max_namespaces: unsupportedRuntimeField, max_tables: unsupportedRuntimeField, max_catalogs: unsupportedRuntimeField, - buckets: unsupportedRuntimeField, + buckets: { + "*": { + decision: unsupportedRuntimeField, + children: {} satisfies Record< + keyof ProjectConfig["storage"]["analytics"]["buckets"][string], + Node + >, + }, + }, } satisfies Record, vector: { enabled: unsupportedRuntimeField, max_buckets: unsupportedRuntimeField, max_indexes: unsupportedRuntimeField, - buckets: unsupportedRuntimeField, + buckets: { + "*": { + decision: unsupportedRuntimeField, + children: {} satisfies Record< + keyof ProjectConfig["storage"]["vector"]["buckets"][string], + Node + >, + }, + }, } satisfies Record, } satisfies Record, studio: { diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts index b94884bcb0..39f79f958b 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -63,15 +63,18 @@ describe("localStackConfigParity", () => { ]); }); - it("preserves raw-document requirements for presence-sensitive sections", () => { + it("preserves presence requirements for presence-sensitive sections", () => { const byPath = new Map(entries.map(({ path, decision }) => [path, decision])); expect(byPath.get("api.auto_expose_new_tables")?.presence).toBe("raw-document"); - expect(byPath.get("auth.external.github.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("auth.external.github.enabled")?.presence).toBe("enabled-subtree"); + expect(byPath.get("auth.external.github.client_id")?.presence).toBe("enabled-subtree"); expect(byPath.get("auth.hook.send_email.enabled")?.presence).toBe("raw-document"); expect(byPath.get("auth.sms.twilio.enabled")?.presence).toBe("raw-document"); expect(byPath.get("storage.image_transformation.enabled")?.presence).toBe("raw-document"); expect(byPath.get("storage.buckets.*")?.presence).toBe("raw-document"); + expect(byPath.get("storage.analytics.buckets.*")?.presence).toBe("raw-document"); + expect(byPath.get("storage.vector.buckets.*")?.presence).toBe("raw-document"); expect(byPath.get("experimental.webhooks.enabled")?.presence).toBe("raw-document"); expect(byPath.get("auth.external.apple.secret")?.presence).toBe("effective-secret"); expect(byPath.get("studio.openai_api_key")?.presence).toBe("effective-secret"); From 4d3f3884d78f392c0dbecc2ba299083c28054cc3 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 20:04:40 +0200 Subject: [PATCH 06/10] test(cli): refine local config presence semantics --- .../next/config/local-stack-config-parity.ts | 118 +++++++++++------- .../local-stack-config-parity.unit.test.ts | 17 ++- 2 files changed, 83 insertions(+), 52 deletions(-) diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts index af5daceec4..e60eabd463 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -11,8 +11,10 @@ import type { ProjectConfig } from "@supabase/config"; */ type LocalStackConfigParityPresence = | "decoded-value" + | "effective-global-secret" | "effective-secret" | "enabled-subtree" + | "non-default-value" | "raw-document"; type LocalStackConfigParityDecision = @@ -73,6 +75,20 @@ const unsupportedSecretRuntimeField: LocalStackConfigParityDecision = { "A concrete resolved secret in an enabled runtime subtree changes local credentials but the next stack launch Adapter does not translate it yet; unresolved generated env placeholders do not count.", }; +const unsupportedGlobalSecretRuntimeField: LocalStackConfigParityDecision = { + _tag: "unsupported-blocking", + presence: "effective-global-secret", + rationale: + "A concrete resolved global credential changes local runtime behavior even when the Auth service is disabled; unresolved generated env placeholders do not count.", +}; + +const unsupportedNonDefaultRuntimeField: LocalStackConfigParityDecision = { + _tag: "unsupported-blocking", + presence: "non-default-value", + rationale: + "Only a value that differs from the generated project-config default changes local runtime behavior.", +}; + const unsupportedEnabledProviderField: LocalStackConfigParityDecision = { _tag: "unsupported-blocking", presence: "enabled-subtree", @@ -203,6 +219,14 @@ const authExternalParity = { zoom: authExternalProviderParity, } satisfies AuthExternalParity; +const authExternalWithCustomParity = { + ...authExternalParity, + "*": { + decision: unsupportedEnabledProviderField, + children: authExternalProviderParity, + }, +} satisfies LocalStackConfigParitySection; + const authHooksParity = { mfa_verification_attempt: authHookParity, password_verification_attempt: authHookParity, @@ -218,31 +242,31 @@ const authSmsParity = { template: unsupportedRuntimeField, max_frequency: unsupportedRuntimeField, twilio: { - enabled: unsupportedRuntimeField, - account_sid: unsupportedRuntimeField, - message_service_sid: unsupportedRuntimeField, + enabled: unsupportedEnabledProviderField, + account_sid: unsupportedEnabledProviderField, + message_service_sid: unsupportedEnabledProviderField, auth_token: unsupportedSecretRuntimeField, } satisfies Record, twilio_verify: { - enabled: unsupportedRuntimeField, - account_sid: unsupportedOptionalRuntimeField, - message_service_sid: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + account_sid: unsupportedEnabledProviderField, + message_service_sid: unsupportedEnabledProviderField, auth_token: unsupportedSecretRuntimeField, } satisfies Record, messagebird: { - enabled: unsupportedRuntimeField, - originator: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + originator: unsupportedEnabledProviderField, access_key: unsupportedSecretRuntimeField, } satisfies Record, textlocal: { - enabled: unsupportedRuntimeField, - sender: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + sender: unsupportedEnabledProviderField, api_key: unsupportedSecretRuntimeField, } satisfies Record, vonage: { - enabled: unsupportedRuntimeField, - from: unsupportedOptionalRuntimeField, - api_key: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + from: unsupportedEnabledProviderField, + api_key: unsupportedEnabledProviderField, api_secret: unsupportedSecretRuntimeField, } satisfies Record, test_otp: unsupportedOptionalRuntimeField, @@ -262,11 +286,11 @@ const authParity = { enable_anonymous_sign_ins: unsupportedRuntimeField, minimum_password_length: unsupportedRuntimeField, password_requirements: unsupportedRuntimeField, - publishable_key: unsupportedSecretRuntimeField, - secret_key: unsupportedSecretRuntimeField, - jwt_secret: unsupportedSecretRuntimeField, - anon_key: unsupportedSecretRuntimeField, - service_role_key: unsupportedSecretRuntimeField, + publishable_key: unsupportedGlobalSecretRuntimeField, + secret_key: unsupportedGlobalSecretRuntimeField, + jwt_secret: unsupportedGlobalSecretRuntimeField, + anon_key: unsupportedGlobalSecretRuntimeField, + service_role_key: unsupportedGlobalSecretRuntimeField, rate_limit: authRateLimitParity, captcha: { enabled: unsupportedRuntimeField, @@ -334,7 +358,7 @@ const authParity = { }, } satisfies Record, sms: authSmsParity, - external: authExternalParity, + external: authExternalWithCustomParity, web3: { solana: { enabled: unsupportedRuntimeField, @@ -344,32 +368,32 @@ const authParity = { } satisfies Record, } satisfies Record, oauth_server: { - enabled: unsupportedRuntimeField, - authorization_url_path: unsupportedRuntimeField, - allow_dynamic_registration: unsupportedRuntimeField, + enabled: unsupportedEnabledProviderField, + authorization_url_path: unsupportedEnabledProviderField, + allow_dynamic_registration: unsupportedEnabledProviderField, } satisfies Record, third_party: { firebase: { - enabled: unsupportedRuntimeField, - project_id: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + project_id: unsupportedEnabledProviderField, } satisfies Record, auth0: { - enabled: unsupportedRuntimeField, - tenant: unsupportedOptionalRuntimeField, - tenant_region: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + tenant: unsupportedEnabledProviderField, + tenant_region: unsupportedEnabledProviderField, } satisfies Record, aws_cognito: { - enabled: unsupportedRuntimeField, - user_pool_id: unsupportedOptionalRuntimeField, - user_pool_region: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + user_pool_id: unsupportedEnabledProviderField, + user_pool_region: unsupportedEnabledProviderField, } satisfies Record, clerk: { - enabled: unsupportedRuntimeField, - domain: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + domain: unsupportedEnabledProviderField, } satisfies Record, workos: { - enabled: unsupportedRuntimeField, - issuer_url: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + issuer_url: unsupportedEnabledProviderField, } satisfies Record, } satisfies Record, } satisfies Record; @@ -429,9 +453,9 @@ const localStackConfigParity = { max_rows: unsupportedRuntimeField, auto_expose_new_tables: mappedAutoExposeNewTables, tls: { - enabled: unsupportedRuntimeField, - cert_path: unsupportedOptionalRuntimeField, - key_path: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledProviderField, + cert_path: unsupportedEnabledProviderField, + key_path: unsupportedEnabledProviderField, } satisfies Record, external_url: unsupportedOptionalRuntimeField, } satisfies Record, @@ -471,7 +495,7 @@ const localStackConfigParity = { enabled: mappedFunctionsDevEdgeRuntime, policy: mappedFunctionsDevEdgeRuntime, inspector_port: mappedFunctionsDevEdgeRuntime, - deno_version: unsupportedRuntimeField, + deno_version: unsupportedNonDefaultRuntimeField, secrets: mappedFunctionsDevEdgeRuntime, } satisfies Record, functions: { @@ -514,13 +538,13 @@ const localStackConfigParity = { enabled: unsupportedRuntimeField, } satisfies Record, analytics: { - enabled: unsupportedRuntimeField, - max_namespaces: unsupportedRuntimeField, - max_tables: unsupportedRuntimeField, - max_catalogs: unsupportedRuntimeField, + enabled: unsupportedEnabledProviderField, + max_namespaces: unsupportedEnabledProviderField, + max_tables: unsupportedEnabledProviderField, + max_catalogs: unsupportedEnabledProviderField, buckets: { "*": { - decision: unsupportedRuntimeField, + decision: unsupportedEnabledProviderField, children: {} satisfies Record< keyof ProjectConfig["storage"]["analytics"]["buckets"][string], Node @@ -529,12 +553,12 @@ const localStackConfigParity = { }, } satisfies Record, vector: { - enabled: unsupportedRuntimeField, - max_buckets: unsupportedRuntimeField, - max_indexes: unsupportedRuntimeField, + enabled: unsupportedEnabledProviderField, + max_buckets: unsupportedEnabledProviderField, + max_indexes: unsupportedEnabledProviderField, buckets: { "*": { - decision: unsupportedRuntimeField, + decision: unsupportedEnabledProviderField, children: {} satisfies Record< keyof ProjectConfig["storage"]["vector"]["buckets"][string], Node diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts index 39f79f958b..dbafe815c1 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -7,7 +7,7 @@ describe("localStackConfigParity", () => { it("classifies every fixed project-config leaf exactly once", () => { const paths = entries.map(({ path }) => path); - expect(paths).toHaveLength(365); + expect(paths).toHaveLength(373); expect(new Set(paths).size).toBe(paths.length); expect( Object.fromEntries( @@ -19,7 +19,7 @@ describe("localStackConfigParity", () => { ).toEqual({ mapped: 11, "not-applicable": 10, - "unsupported-blocking": 338, + "unsupported-blocking": 346, "unsupported-warning": 6, }); }); @@ -70,11 +70,18 @@ describe("localStackConfigParity", () => { expect(byPath.get("auth.external.github.enabled")?.presence).toBe("enabled-subtree"); expect(byPath.get("auth.external.github.client_id")?.presence).toBe("enabled-subtree"); expect(byPath.get("auth.hook.send_email.enabled")?.presence).toBe("raw-document"); - expect(byPath.get("auth.sms.twilio.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("auth.sms.twilio.enabled")?.presence).toBe("enabled-subtree"); + expect(byPath.get("auth.oauth_server.enabled")?.presence).toBe("enabled-subtree"); + expect(byPath.get("auth.third_party.firebase.project_id")?.presence).toBe("enabled-subtree"); + expect(byPath.get("api.tls.cert_path")?.presence).toBe("enabled-subtree"); + expect(byPath.get("storage.analytics.max_tables")?.presence).toBe("enabled-subtree"); + expect(byPath.get("edge_runtime.deno_version")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.jwt_secret")?.presence).toBe("effective-global-secret"); + expect(byPath.get("auth.external.*")?.presence).toBe("enabled-subtree"); expect(byPath.get("storage.image_transformation.enabled")?.presence).toBe("raw-document"); expect(byPath.get("storage.buckets.*")?.presence).toBe("raw-document"); - expect(byPath.get("storage.analytics.buckets.*")?.presence).toBe("raw-document"); - expect(byPath.get("storage.vector.buckets.*")?.presence).toBe("raw-document"); + expect(byPath.get("storage.analytics.buckets.*")?.presence).toBe("enabled-subtree"); + expect(byPath.get("storage.vector.buckets.*")?.presence).toBe("enabled-subtree"); expect(byPath.get("experimental.webhooks.enabled")?.presence).toBe("raw-document"); expect(byPath.get("auth.external.apple.secret")?.presence).toBe("effective-secret"); expect(byPath.get("studio.openai_api_key")?.presence).toBe("effective-secret"); From 94698466f7b1f38c6f37ff6758437e8c98a229f1 Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 20:53:31 +0200 Subject: [PATCH 07/10] fix(cli): classify generated config defaults --- .../next/config/local-stack-config-parity.ts | 49 +++++++++++-------- .../local-stack-config-parity.unit.test.ts | 6 ++- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts index e60eabd463..f0b75be733 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -143,6 +143,13 @@ const commandOnlyDatabaseField: LocalStackConfigParityDecision = { "This field configures database tooling outside local stack startup and does not belong in StackConfig.", }; +const hostedConfigurationField: LocalStackConfigParityDecision = { + _tag: "not-applicable", + presence: "decoded-value", + rationale: + "This hosted-service limit is used by configuration management but does not change the local stack runtime.", +}; + const projectMetadataField: LocalStackConfigParityDecision = { _tag: "not-applicable", presence: "raw-document", @@ -237,10 +244,10 @@ const authHooksParity = { } satisfies Record; const authSmsParity = { - enable_signup: unsupportedRuntimeField, - enable_confirmations: unsupportedRuntimeField, - template: unsupportedRuntimeField, - max_frequency: unsupportedRuntimeField, + enable_signup: unsupportedEnabledProviderField, + enable_confirmations: unsupportedEnabledProviderField, + template: unsupportedEnabledProviderField, + max_frequency: unsupportedEnabledProviderField, twilio: { enabled: unsupportedEnabledProviderField, account_sid: unsupportedEnabledProviderField, @@ -437,20 +444,20 @@ const dbSettingsParity = { const localStackConfigParity = { project_id: projectMetadataField, analytics: { - enabled: unsupportedRuntimeField, - port: unsupportedRuntimeField, - backend: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, + port: unsupportedNonDefaultRuntimeField, + backend: unsupportedNonDefaultRuntimeField, vector_port: unsupportedOptionalRuntimeField, gcp_project_id: unsupportedOptionalRuntimeField, gcp_project_number: unsupportedOptionalRuntimeField, gcp_jwt_path: unsupportedOptionalRuntimeField, } satisfies Record, api: { - enabled: unsupportedRuntimeField, - port: unsupportedRuntimeField, - schemas: unsupportedRuntimeField, - extra_search_path: unsupportedRuntimeField, - max_rows: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, + port: unsupportedNonDefaultRuntimeField, + schemas: unsupportedNonDefaultRuntimeField, + extra_search_path: unsupportedNonDefaultRuntimeField, + max_rows: unsupportedNonDefaultRuntimeField, auto_expose_new_tables: mappedAutoExposeNewTables, tls: { enabled: unsupportedEnabledProviderField, @@ -505,21 +512,21 @@ const localStackConfigParity = { }, }, local_smtp: { - enabled: unsupportedRuntimeField, - port: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, + port: unsupportedNonDefaultRuntimeField, smtp_port: unsupportedOptionalRuntimeField, pop3_port: unsupportedOptionalRuntimeField, admin_email: unsupportedOptionalRuntimeField, sender_name: unsupportedOptionalRuntimeField, } satisfies Record, realtime: { - enabled: unsupportedRuntimeField, - ip_version: unsupportedRuntimeField, - max_header_length: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, + ip_version: unsupportedNonDefaultRuntimeField, + max_header_length: unsupportedNonDefaultRuntimeField, } satisfies Record, storage: { - enabled: unsupportedRuntimeField, - file_size_limit: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, + file_size_limit: unsupportedNonDefaultRuntimeField, image_transformation: { enabled: unsupportedRuntimeField, } satisfies Record, Node>, @@ -554,8 +561,8 @@ const localStackConfigParity = { } satisfies Record, vector: { enabled: unsupportedEnabledProviderField, - max_buckets: unsupportedEnabledProviderField, - max_indexes: unsupportedEnabledProviderField, + max_buckets: hostedConfigurationField, + max_indexes: hostedConfigurationField, buckets: { "*": { decision: unsupportedEnabledProviderField, diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts index dbafe815c1..13f0f7919c 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -18,8 +18,8 @@ describe("localStackConfigParity", () => { ), ).toEqual({ mapped: 11, - "not-applicable": 10, - "unsupported-blocking": 346, + "not-applicable": 12, + "unsupported-blocking": 344, "unsupported-warning": 6, }); }); @@ -104,6 +104,8 @@ describe("localStackConfigParity", () => { "experimental.pgdelta.format_options", "project_id", "remotes", + "storage.vector.max_buckets", + "storage.vector.max_indexes", ]); }); }); From bf2289a5281f17fc0b859f6a79ce1bcd54a444db Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 21:15:31 +0200 Subject: [PATCH 08/10] test(cli): ignore generated stack defaults --- apps/cli/src/next/config/local-stack-config-parity.ts | 10 +++++----- .../next/config/local-stack-config-parity.unit.test.ts | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts index f0b75be733..686a084e45 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -283,7 +283,7 @@ const authParity = { enabled: unsupportedRuntimeField, site_url: unsupportedRuntimeField, additional_redirect_urls: unsupportedRuntimeField, - jwt_expiry: unsupportedRuntimeField, + jwt_expiry: unsupportedNonDefaultRuntimeField, jwt_issuer: unsupportedOptionalRuntimeField, signing_keys_path: unsupportedOptionalRuntimeField, enable_refresh_token_rotation: unsupportedRuntimeField, @@ -468,7 +468,7 @@ const localStackConfigParity = { } satisfies Record, auth: authParity, db: { - port: unsupportedRuntimeField, + port: unsupportedNonDefaultRuntimeField, shadow_port: commandOnlyDatabaseField, health_timeout: unsupportedRuntimeField, major_version: unsupportedRuntimeField, @@ -542,7 +542,7 @@ const localStackConfigParity = { }, }, s3_protocol: { - enabled: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, } satisfies Record, analytics: { enabled: unsupportedEnabledProviderField, @@ -575,8 +575,8 @@ const localStackConfigParity = { } satisfies Record, } satisfies Record, studio: { - enabled: unsupportedRuntimeField, - port: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, + port: unsupportedNonDefaultRuntimeField, api_url: unsupportedRuntimeField, openai_api_key: unsupportedSecretRuntimeField, } satisfies Record, diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts index 13f0f7919c..0b6daeac6a 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -76,6 +76,11 @@ describe("localStackConfigParity", () => { expect(byPath.get("api.tls.cert_path")?.presence).toBe("enabled-subtree"); expect(byPath.get("storage.analytics.max_tables")?.presence).toBe("enabled-subtree"); expect(byPath.get("edge_runtime.deno_version")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.jwt_expiry")?.presence).toBe("non-default-value"); + expect(byPath.get("db.port")?.presence).toBe("non-default-value"); + expect(byPath.get("storage.s3_protocol.enabled")?.presence).toBe("non-default-value"); + expect(byPath.get("studio.enabled")?.presence).toBe("non-default-value"); + expect(byPath.get("studio.port")?.presence).toBe("non-default-value"); expect(byPath.get("auth.jwt_secret")?.presence).toBe("effective-global-secret"); expect(byPath.get("auth.external.*")?.presence).toBe("enabled-subtree"); expect(byPath.get("storage.image_transformation.enabled")?.presence).toBe("raw-document"); From 70d97dadd6642475d15a190825a09817f50dd39c Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 21:35:08 +0200 Subject: [PATCH 09/10] fix(cli): complete local config parity inventory --- .../next/config/local-stack-config-parity.ts | 44 +++++++++++++------ .../local-stack-config-parity.unit.test.ts | 16 +++++-- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts index 686a084e45..58cf0e5713 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -150,6 +150,13 @@ const hostedConfigurationField: LocalStackConfigParityDecision = { "This hosted-service limit is used by configuration management but does not change the local stack runtime.", }; +const legacyIgnoredLocalRuntimeField: LocalStackConfigParityDecision = { + _tag: "not-applicable", + presence: "raw-document", + rationale: + "The legacy local start runtime ignores this field, so it cannot change local stack behavior.", +}; + const projectMetadataField: LocalStackConfigParityDecision = { _tag: "not-applicable", presence: "raw-document", @@ -195,7 +202,7 @@ const authHookParity = { } satisfies Record; const authRateLimitParity = { - email_sent: unsupportedRuntimeField, + email_sent: legacyIgnoredLocalRuntimeField, sms_sent: unsupportedRuntimeField, anonymous_users: unsupportedRuntimeField, token_refresh: unsupportedRuntimeField, @@ -280,7 +287,16 @@ const authSmsParity = { } satisfies Record; const authParity = { - enabled: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, + external_url: unsupportedRuntimeField, + passkey: { + enabled: unsupportedEnabledProviderField, + }, + webauthn: { + rp_display_name: unsupportedRuntimeField, + rp_id: unsupportedRuntimeField, + rp_origins: unsupportedRuntimeField, + }, site_url: unsupportedRuntimeField, additional_redirect_urls: unsupportedRuntimeField, jwt_expiry: unsupportedNonDefaultRuntimeField, @@ -289,7 +305,7 @@ const authParity = { enable_refresh_token_rotation: unsupportedRuntimeField, refresh_token_reuse_interval: unsupportedRuntimeField, enable_manual_linking: unsupportedRuntimeField, - enable_signup: unsupportedRuntimeField, + enable_signup: unsupportedNonDefaultRuntimeField, enable_anonymous_sign_ins: unsupportedRuntimeField, minimum_password_length: unsupportedRuntimeField, password_requirements: unsupportedRuntimeField, @@ -328,13 +344,13 @@ const authParity = { inactivity_timeout: unsupportedOptionalRuntimeField, } satisfies Record, Node>, email: { - enable_signup: unsupportedRuntimeField, - double_confirm_changes: unsupportedRuntimeField, - enable_confirmations: unsupportedRuntimeField, - secure_password_change: unsupportedRuntimeField, - max_frequency: unsupportedRuntimeField, - otp_length: unsupportedRuntimeField, - otp_expiry: unsupportedRuntimeField, + enable_signup: unsupportedNonDefaultRuntimeField, + double_confirm_changes: unsupportedNonDefaultRuntimeField, + enable_confirmations: unsupportedNonDefaultRuntimeField, + secure_password_change: unsupportedNonDefaultRuntimeField, + max_frequency: unsupportedNonDefaultRuntimeField, + otp_length: unsupportedNonDefaultRuntimeField, + otp_expiry: unsupportedNonDefaultRuntimeField, smtp: { enabled: unsupportedRuntimeField, host: unsupportedOptionalRuntimeField, @@ -368,10 +384,10 @@ const authParity = { external: authExternalWithCustomParity, web3: { solana: { - enabled: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, } satisfies Record, ethereum: { - enabled: unsupportedRuntimeField, + enabled: unsupportedNonDefaultRuntimeField, } satisfies Record, } satisfies Record, oauth_server: { @@ -403,7 +419,7 @@ const authParity = { issuer_url: unsupportedEnabledProviderField, } satisfies Record, } satisfies Record, -} satisfies Record; +} satisfies Record & LocalStackConfigParitySection; const dbSettingsParity = { effective_cache_size: unsupportedOptionalRuntimeField, @@ -471,7 +487,7 @@ const localStackConfigParity = { port: unsupportedNonDefaultRuntimeField, shadow_port: commandOnlyDatabaseField, health_timeout: unsupportedRuntimeField, - major_version: unsupportedRuntimeField, + major_version: unsupportedNonDefaultRuntimeField, pooler: { enabled: unsupportedRuntimeField, port: unsupportedRuntimeField, diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts index 0b6daeac6a..c4a3a3effe 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -7,7 +7,7 @@ describe("localStackConfigParity", () => { it("classifies every fixed project-config leaf exactly once", () => { const paths = entries.map(({ path }) => path); - expect(paths).toHaveLength(373); + expect(paths).toHaveLength(378); expect(new Set(paths).size).toBe(paths.length); expect( Object.fromEntries( @@ -18,8 +18,8 @@ describe("localStackConfigParity", () => { ), ).toEqual({ mapped: 11, - "not-applicable": 12, - "unsupported-blocking": 344, + "not-applicable": 13, + "unsupported-blocking": 348, "unsupported-warning": 6, }); }); @@ -77,6 +77,15 @@ describe("localStackConfigParity", () => { expect(byPath.get("storage.analytics.max_tables")?.presence).toBe("enabled-subtree"); expect(byPath.get("edge_runtime.deno_version")?.presence).toBe("non-default-value"); expect(byPath.get("auth.jwt_expiry")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.enabled")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.enable_signup")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.email.enable_signup")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.email.enable_confirmations")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.web3.solana.enabled")?.presence).toBe("non-default-value"); + expect(byPath.get("db.major_version")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.external_url")?.presence).toBe("raw-document"); + expect(byPath.get("auth.passkey.enabled")?.presence).toBe("enabled-subtree"); + expect(byPath.get("auth.webauthn.rp_id")?.presence).toBe("raw-document"); expect(byPath.get("db.port")?.presence).toBe("non-default-value"); expect(byPath.get("storage.s3_protocol.enabled")?.presence).toBe("non-default-value"); expect(byPath.get("studio.enabled")?.presence).toBe("non-default-value"); @@ -99,6 +108,7 @@ describe("localStackConfigParity", () => { .map(({ path }) => path) .sort(), ).toEqual([ + "auth.rate_limit.email_sent", "db.network_restrictions.allowed_cidrs", "db.network_restrictions.allowed_cidrs_v6", "db.network_restrictions.enabled", From de9db547680f15c187ba58f1600ea9e4b8dcf45a Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Wed, 5 Aug 2026 21:52:53 +0200 Subject: [PATCH 10/10] test(cli): refine parity presence semantics --- .../next/config/local-stack-config-parity.ts | 142 +++++++++--------- .../local-stack-config-parity.unit.test.ts | 10 +- 2 files changed, 80 insertions(+), 72 deletions(-) diff --git a/apps/cli/src/next/config/local-stack-config-parity.ts b/apps/cli/src/next/config/local-stack-config-parity.ts index 58cf0e5713..87c9333cb9 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.ts @@ -89,11 +89,11 @@ const unsupportedNonDefaultRuntimeField: LocalStackConfigParityDecision = { "Only a value that differs from the generated project-config default changes local runtime behavior.", }; -const unsupportedEnabledProviderField: LocalStackConfigParityDecision = { +const unsupportedEnabledSubtreeField: LocalStackConfigParityDecision = { _tag: "unsupported-blocking", presence: "enabled-subtree", rationale: - "This setting changes local authentication behavior only when its provider is effectively enabled; generated disabled provider stubs do not count.", + "This setting changes local runtime behavior only when its enclosing feature is effectively enabled; generated disabled stubs do not count.", }; const mappedAutoExposeNewTables: LocalStackConfigParityDecision = { @@ -179,13 +179,13 @@ const unsupportedFutureRuntimeField: LocalStackConfigParityDecision = { }; const authExternalProviderParity = { - enabled: unsupportedEnabledProviderField, - client_id: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + client_id: unsupportedEnabledSubtreeField, secret: unsupportedSecretRuntimeField, - url: unsupportedEnabledProviderField, - redirect_uri: unsupportedEnabledProviderField, - skip_nonce_check: unsupportedEnabledProviderField, - email_optional: unsupportedEnabledProviderField, + url: unsupportedEnabledSubtreeField, + redirect_uri: unsupportedEnabledSubtreeField, + skip_nonce_check: unsupportedEnabledSubtreeField, + email_optional: unsupportedEnabledSubtreeField, } satisfies Record; type AuthExternalParity = { @@ -196,8 +196,8 @@ type AuthExternalParity = { }; const authHookParity = { - enabled: unsupportedRuntimeField, - uri: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledSubtreeField, + uri: unsupportedEnabledSubtreeField, secrets: unsupportedSecretRuntimeField, } satisfies Record; @@ -236,7 +236,7 @@ const authExternalParity = { const authExternalWithCustomParity = { ...authExternalParity, "*": { - decision: unsupportedEnabledProviderField, + decision: unsupportedEnabledSubtreeField, children: authExternalProviderParity, }, } satisfies LocalStackConfigParitySection; @@ -251,36 +251,36 @@ const authHooksParity = { } satisfies Record; const authSmsParity = { - enable_signup: unsupportedEnabledProviderField, - enable_confirmations: unsupportedEnabledProviderField, - template: unsupportedEnabledProviderField, - max_frequency: unsupportedEnabledProviderField, + enable_signup: unsupportedEnabledSubtreeField, + enable_confirmations: unsupportedEnabledSubtreeField, + template: unsupportedEnabledSubtreeField, + max_frequency: unsupportedEnabledSubtreeField, twilio: { - enabled: unsupportedEnabledProviderField, - account_sid: unsupportedEnabledProviderField, - message_service_sid: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + account_sid: unsupportedEnabledSubtreeField, + message_service_sid: unsupportedEnabledSubtreeField, auth_token: unsupportedSecretRuntimeField, } satisfies Record, twilio_verify: { - enabled: unsupportedEnabledProviderField, - account_sid: unsupportedEnabledProviderField, - message_service_sid: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + account_sid: unsupportedEnabledSubtreeField, + message_service_sid: unsupportedEnabledSubtreeField, auth_token: unsupportedSecretRuntimeField, } satisfies Record, messagebird: { - enabled: unsupportedEnabledProviderField, - originator: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + originator: unsupportedEnabledSubtreeField, access_key: unsupportedSecretRuntimeField, } satisfies Record, textlocal: { - enabled: unsupportedEnabledProviderField, - sender: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + sender: unsupportedEnabledSubtreeField, api_key: unsupportedSecretRuntimeField, } satisfies Record, vonage: { - enabled: unsupportedEnabledProviderField, - from: unsupportedEnabledProviderField, - api_key: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + from: unsupportedEnabledSubtreeField, + api_key: unsupportedEnabledSubtreeField, api_secret: unsupportedSecretRuntimeField, } satisfies Record, test_otp: unsupportedOptionalRuntimeField, @@ -290,7 +290,7 @@ const authParity = { enabled: unsupportedNonDefaultRuntimeField, external_url: unsupportedRuntimeField, passkey: { - enabled: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, }, webauthn: { rp_display_name: unsupportedRuntimeField, @@ -323,21 +323,21 @@ const authParity = { hook: authHooksParity, mfa: { totp: { - enroll_enabled: unsupportedRuntimeField, - verify_enabled: unsupportedRuntimeField, + enroll_enabled: unsupportedNonDefaultRuntimeField, + verify_enabled: unsupportedNonDefaultRuntimeField, } satisfies Record, phone: { - enroll_enabled: unsupportedRuntimeField, - verify_enabled: unsupportedRuntimeField, - otp_length: unsupportedRuntimeField, - template: unsupportedRuntimeField, - max_frequency: unsupportedRuntimeField, + enroll_enabled: unsupportedNonDefaultRuntimeField, + verify_enabled: unsupportedNonDefaultRuntimeField, + otp_length: unsupportedEnabledSubtreeField, + template: unsupportedEnabledSubtreeField, + max_frequency: unsupportedEnabledSubtreeField, } satisfies Record, web_authn: { - enroll_enabled: unsupportedRuntimeField, - verify_enabled: unsupportedRuntimeField, + enroll_enabled: unsupportedNonDefaultRuntimeField, + verify_enabled: unsupportedNonDefaultRuntimeField, } satisfies Record, - max_enrolled_factors: unsupportedRuntimeField, + max_enrolled_factors: unsupportedNonDefaultRuntimeField, } satisfies Record, sessions: { timebox: unsupportedOptionalRuntimeField, @@ -352,13 +352,13 @@ const authParity = { otp_length: unsupportedNonDefaultRuntimeField, otp_expiry: unsupportedNonDefaultRuntimeField, smtp: { - enabled: unsupportedRuntimeField, - host: unsupportedOptionalRuntimeField, - port: unsupportedOptionalRuntimeField, - user: unsupportedOptionalRuntimeField, + enabled: unsupportedEnabledSubtreeField, + host: unsupportedEnabledSubtreeField, + port: unsupportedEnabledSubtreeField, + user: unsupportedEnabledSubtreeField, pass: unsupportedSecretRuntimeField, - admin_email: unsupportedOptionalRuntimeField, - sender_name: unsupportedOptionalRuntimeField, + admin_email: unsupportedEnabledSubtreeField, + sender_name: unsupportedEnabledSubtreeField, } satisfies Record, Node>, template: { "*": { @@ -391,32 +391,32 @@ const authParity = { } satisfies Record, } satisfies Record, oauth_server: { - enabled: unsupportedEnabledProviderField, - authorization_url_path: unsupportedEnabledProviderField, - allow_dynamic_registration: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + authorization_url_path: unsupportedEnabledSubtreeField, + allow_dynamic_registration: unsupportedEnabledSubtreeField, } satisfies Record, third_party: { firebase: { - enabled: unsupportedEnabledProviderField, - project_id: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + project_id: unsupportedEnabledSubtreeField, } satisfies Record, auth0: { - enabled: unsupportedEnabledProviderField, - tenant: unsupportedEnabledProviderField, - tenant_region: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + tenant: unsupportedEnabledSubtreeField, + tenant_region: unsupportedEnabledSubtreeField, } satisfies Record, aws_cognito: { - enabled: unsupportedEnabledProviderField, - user_pool_id: unsupportedEnabledProviderField, - user_pool_region: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + user_pool_id: unsupportedEnabledSubtreeField, + user_pool_region: unsupportedEnabledSubtreeField, } satisfies Record, clerk: { - enabled: unsupportedEnabledProviderField, - domain: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + domain: unsupportedEnabledSubtreeField, } satisfies Record, workos: { - enabled: unsupportedEnabledProviderField, - issuer_url: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + issuer_url: unsupportedEnabledSubtreeField, } satisfies Record, } satisfies Record, } satisfies Record & LocalStackConfigParitySection; @@ -476,9 +476,9 @@ const localStackConfigParity = { max_rows: unsupportedNonDefaultRuntimeField, auto_expose_new_tables: mappedAutoExposeNewTables, tls: { - enabled: unsupportedEnabledProviderField, - cert_path: unsupportedEnabledProviderField, - key_path: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + cert_path: unsupportedEnabledSubtreeField, + key_path: unsupportedEnabledSubtreeField, } satisfies Record, external_url: unsupportedOptionalRuntimeField, } satisfies Record, @@ -561,13 +561,13 @@ const localStackConfigParity = { enabled: unsupportedNonDefaultRuntimeField, } satisfies Record, analytics: { - enabled: unsupportedEnabledProviderField, - max_namespaces: unsupportedEnabledProviderField, - max_tables: unsupportedEnabledProviderField, - max_catalogs: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, + max_namespaces: unsupportedEnabledSubtreeField, + max_tables: unsupportedEnabledSubtreeField, + max_catalogs: unsupportedEnabledSubtreeField, buckets: { "*": { - decision: unsupportedEnabledProviderField, + decision: unsupportedEnabledSubtreeField, children: {} satisfies Record< keyof ProjectConfig["storage"]["analytics"]["buckets"][string], Node @@ -576,12 +576,12 @@ const localStackConfigParity = { }, } satisfies Record, vector: { - enabled: unsupportedEnabledProviderField, + enabled: unsupportedEnabledSubtreeField, max_buckets: hostedConfigurationField, max_indexes: hostedConfigurationField, buckets: { "*": { - decision: unsupportedEnabledProviderField, + decision: unsupportedEnabledSubtreeField, children: {} satisfies Record< keyof ProjectConfig["storage"]["vector"]["buckets"][string], Node @@ -593,7 +593,7 @@ const localStackConfigParity = { studio: { enabled: unsupportedNonDefaultRuntimeField, port: unsupportedNonDefaultRuntimeField, - api_url: unsupportedRuntimeField, + api_url: unsupportedNonDefaultRuntimeField, openai_api_key: unsupportedSecretRuntimeField, } satisfies Record, experimental: { diff --git a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts index c4a3a3effe..e3ba1a31dc 100644 --- a/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts +++ b/apps/cli/src/next/config/local-stack-config-parity.unit.test.ts @@ -69,7 +69,8 @@ describe("localStackConfigParity", () => { expect(byPath.get("api.auto_expose_new_tables")?.presence).toBe("raw-document"); expect(byPath.get("auth.external.github.enabled")?.presence).toBe("enabled-subtree"); expect(byPath.get("auth.external.github.client_id")?.presence).toBe("enabled-subtree"); - expect(byPath.get("auth.hook.send_email.enabled")?.presence).toBe("raw-document"); + expect(byPath.get("auth.hook.send_email.enabled")?.presence).toBe("enabled-subtree"); + expect(byPath.get("auth.hook.send_email.uri")?.presence).toBe("enabled-subtree"); expect(byPath.get("auth.sms.twilio.enabled")?.presence).toBe("enabled-subtree"); expect(byPath.get("auth.oauth_server.enabled")?.presence).toBe("enabled-subtree"); expect(byPath.get("auth.third_party.firebase.project_id")?.presence).toBe("enabled-subtree"); @@ -81,7 +82,13 @@ describe("localStackConfigParity", () => { expect(byPath.get("auth.enable_signup")?.presence).toBe("non-default-value"); expect(byPath.get("auth.email.enable_signup")?.presence).toBe("non-default-value"); expect(byPath.get("auth.email.enable_confirmations")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.email.smtp.enabled")?.presence).toBe("enabled-subtree"); + expect(byPath.get("auth.email.smtp.host")?.presence).toBe("enabled-subtree"); + expect(byPath.get("auth.email.smtp.pass")?.presence).toBe("effective-secret"); expect(byPath.get("auth.web3.solana.enabled")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.mfa.totp.enroll_enabled")?.presence).toBe("non-default-value"); + expect(byPath.get("auth.mfa.phone.otp_length")?.presence).toBe("enabled-subtree"); + expect(byPath.get("auth.mfa.max_enrolled_factors")?.presence).toBe("non-default-value"); expect(byPath.get("db.major_version")?.presence).toBe("non-default-value"); expect(byPath.get("auth.external_url")?.presence).toBe("raw-document"); expect(byPath.get("auth.passkey.enabled")?.presence).toBe("enabled-subtree"); @@ -90,6 +97,7 @@ describe("localStackConfigParity", () => { expect(byPath.get("storage.s3_protocol.enabled")?.presence).toBe("non-default-value"); expect(byPath.get("studio.enabled")?.presence).toBe("non-default-value"); expect(byPath.get("studio.port")?.presence).toBe("non-default-value"); + expect(byPath.get("studio.api_url")?.presence).toBe("non-default-value"); expect(byPath.get("auth.jwt_secret")?.presence).toBe("effective-global-secret"); expect(byPath.get("auth.external.*")?.presence).toBe("enabled-subtree"); expect(byPath.get("storage.image_transformation.enabled")?.presence).toBe("raw-document");