99 * always prefers the project's current (`remote`) value over a schema-materialized local
1010 * default, so an undeclared companion never gets clobbered with a default the user never asked
1111 * for. Only when `remote` doesn't report a companion at all does resolution fall through to
12- * `local` (and, for the storage feature containers only, the decoded `config`) — and that
13- * fallback is disclosed via `forced`, never applied silently.
12+ * `local` (and, for the storage feature containers and the OAuth server pair only, the decoded
13+ * `config`) — and that fallback is disclosed via `forced`, never applied silently.
1414 */
1515
1616import type { CliConfig , ConfigChange , ProjectConfig } from "@supabase/config" ;
@@ -53,7 +53,8 @@ export interface PushEncoderInput {
5353/**
5454 * The storage encoder's own input: adds the decoded `config` as the last resort for the
5555 * `storage.analytics`/`storage.vector` containers' required inner keys, which the local
56- * projection prunes entirely once disabled. No other encoder needs this fourth tier.
56+ * projection prunes entirely once disabled. Only the auth encoder's OAuth server pair also
57+ * needs this fourth tier.
5758 */
5859export interface StorageEncoderInput extends PushEncoderInput {
5960 readonly config : CliConfig ;
@@ -125,8 +126,11 @@ export interface AuthEncoderInput extends PushEncoderInput {
125126 readonly secrets : ReadonlyArray < PushSecretDecision > ;
126127 /** HTML bodies loaded from `content_path`, for the unmapped mailer `*_content` keys. */
127128 readonly emailContent : AuthEmailContent ;
128- /** Raw `data.attributes.auth` — for comparing the 13 unmapped mailer `*_content` keys. */
129+ /** Raw `data.attributes.auth` — for comparing the 13 unmapped mailer `*_content` keys, and
130+ * for the OAuth server's stored path that `remote` prunes while disabled. */
129131 readonly remoteAuthAttributes : Readonly < Record < string , unknown > > ;
132+ /** Decoded config — the OAuth server pair's last fallback when the projection pruned it. */
133+ readonly config : CliConfig ;
130134 /** Clock value for `sms_test_otp_valid_until` (+10 calendar years). */
131135 readonly now : DateTime . Utc ;
132136}
@@ -201,12 +205,11 @@ function resolveLeaf(
201205}
202206
203207/**
204- * {@link resolveLeaf }, with a fourth fallback tier for the storage feature
205- * containers only: the decoded `config`'s schema-materialized value, for the
206- * `max_*` keys the local projection prunes entirely once a feature is
207- * disabled.
208+ * {@link resolveLeaf }, with a fourth fallback tier: the decoded `config`'s
209+ * schema-materialized value, for keys the local projection prunes entirely
210+ * once a feature is disabled.
208211 */
209- function resolveStorageFeatureLeaf (
212+ function resolvePrunedLeaf (
210213 changes : ReadonlyArray < ConfigChange > ,
211214 path : ReadonlyArray < string > ,
212215 remote : ProjectConfig ,
@@ -642,28 +645,28 @@ function encodeStorageBodyImpl(input: StorageEncoderInput): PushEncoded<StorageU
642645 const analyticsChanges = changesUnderPrefix ( changes , [ "storage" , "analytics" ] ) ;
643646 if ( analyticsChanges . length > 0 ) {
644647 const analyticsPaths = analyticsChanges . map ( ( change ) => change . path ) ;
645- const enabledR = resolveStorageFeatureLeaf (
648+ const enabledR = resolvePrunedLeaf (
646649 changes ,
647650 [ "storage" , "analytics" , "enabled" ] ,
648651 remote ,
649652 local ,
650653 config ,
651654 ) ;
652- const maxNamespacesR = resolveStorageFeatureLeaf (
655+ const maxNamespacesR = resolvePrunedLeaf (
653656 changes ,
654657 [ "storage" , "analytics" , "max_namespaces" ] ,
655658 remote ,
656659 local ,
657660 config ,
658661 ) ;
659- const maxTablesR = resolveStorageFeatureLeaf (
662+ const maxTablesR = resolvePrunedLeaf (
660663 changes ,
661664 [ "storage" , "analytics" , "max_tables" ] ,
662665 remote ,
663666 local ,
664667 config ,
665668 ) ;
666- const maxCatalogsR = resolveStorageFeatureLeaf (
669+ const maxCatalogsR = resolvePrunedLeaf (
667670 changes ,
668671 [ "storage" , "analytics" , "max_catalogs" ] ,
669672 remote ,
@@ -697,21 +700,21 @@ function encodeStorageBodyImpl(input: StorageEncoderInput): PushEncoded<StorageU
697700 const vectorChanges = changesUnderPrefix ( changes , [ "storage" , "vector" ] ) ;
698701 if ( vectorChanges . length > 0 ) {
699702 const vectorPaths = vectorChanges . map ( ( change ) => change . path ) ;
700- const enabledR = resolveStorageFeatureLeaf (
703+ const enabledR = resolvePrunedLeaf (
701704 changes ,
702705 [ "storage" , "vector" , "enabled" ] ,
703706 remote ,
704707 local ,
705708 config ,
706709 ) ;
707- const maxBucketsR = resolveStorageFeatureLeaf (
710+ const maxBucketsR = resolvePrunedLeaf (
708711 changes ,
709712 [ "storage" , "vector" , "max_buckets" ] ,
710713 remote ,
711714 local ,
712715 config ,
713716 ) ;
714- const maxIndexesR = resolveStorageFeatureLeaf (
717+ const maxIndexesR = resolvePrunedLeaf (
715718 changes ,
716719 [ "storage" , "vector" , "max_indexes" ] ,
717720 remote ,
@@ -1373,7 +1376,8 @@ export const PUSH_AUTH_LEAF_MAP: ReadonlyArray<PushAuthLeafSpec> = [
13731376function encodeAuthBodyImpl (
13741377 input : AuthEncoderInput ,
13751378) : PushEncoded < Readonly < Record < string , unknown > > > {
1376- const { changes, local, remote, secrets, emailContent, remoteAuthAttributes, now } = input ;
1379+ const { changes, local, remote, secrets, emailContent, remoteAuthAttributes, config, now } =
1380+ input ;
13771381 const body : Record < string , unknown > = { } ;
13781382 const encoded : Array < ReadonlyArray < string > > = [ ] ;
13791383 const unencodable : Array < { path : ReadonlyArray < string > ; reason : string } > = [ ] ;
@@ -1388,6 +1392,24 @@ function encodeAuthBodyImpl(
13881392 for ( const spec of PUSH_AUTH_LEAF_MAP ) {
13891393 leaf ( spec . configPath , spec . apiKey , spec . transform , spec . reason ) ;
13901394 }
1395+ /* The API takes `oauth_server_enabled` and `oauth_server_authorization_path` only together, so
1396+ when one of them ships the other rides along with its resolved value. */
1397+ const oauthPair = PUSH_AUTH_LEAF_MAP . filter (
1398+ ( spec ) =>
1399+ spec . apiKey === "oauth_server_enabled" || spec . apiKey === "oauth_server_authorization_path" ,
1400+ ) ;
1401+ const oauthSent = oauthPair . find ( ( spec ) => spec . apiKey in body ) ;
1402+ const oauthUnsent = oauthPair . find ( ( spec ) => ! ( spec . apiKey in body ) ) ;
1403+ if ( oauthSent !== undefined && oauthUnsent !== undefined ) {
1404+ /* `remote` prunes a disabled server's stored path, so the raw attribute is read first. */
1405+ const stored = oauthUnsent . transform ( remoteAuthAttributes [ oauthUnsent . apiKey ] ) ;
1406+ const resolved : LeafResolution =
1407+ stored === undefined
1408+ ? resolvePrunedLeaf ( changes , oauthUnsent . configPath , remote , local , config )
1409+ : { value : stored , source : "remote" } ;
1410+ body [ oauthUnsent . apiKey ] = resolved . value ;
1411+ pushForced ( forced , oauthUnsent . configPath , resolved ) ;
1412+ }
13911413
13921414 // smtp (container)
13931415 const smtpChanges = changesUnderPrefix ( changes , [ "auth" , "email" , "smtp" ] ) ;
0 commit comments