From 9b9a5dfef1df147282c6fada2405775ac176c5fd Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:28:46 +0200 Subject: [PATCH 01/18] feat(auth): add redirect configuration model --- src/authRedirects.ts | 116 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/authRedirects.ts diff --git a/src/authRedirects.ts b/src/authRedirects.ts new file mode 100644 index 0000000..ae8893f --- /dev/null +++ b/src/authRedirects.ts @@ -0,0 +1,116 @@ +export type AuthRedirectEnvironment = 'local' | 'preview' | 'production'; + +export interface ResolveAuthRedirectConfigurationInput { + environment: AuthRedirectEnvironment; + gatewayOrigin: string; + siteOrigin: string; + callbackRoute: string; + webOrigins?: readonly string[]; + nativeRedirectUris?: readonly string[]; +} + +export interface AuthRedirectConfiguration { + providerCallbackUrl: string; + siteUrl: string; + redirectAllowList: readonly string[]; + serializedRedirectAllowList: string; +} + +export function resolveAuthRedirectConfiguration( + input: ResolveAuthRedirectConfigurationInput, +): AuthRedirectConfiguration { + const gatewayOrigin = normalizeWebOrigin(input.gatewayOrigin, 'gatewayOrigin'); + const siteUrl = normalizeWebOrigin(input.siteOrigin, 'siteOrigin'); + const callbackRoute = normalizeCallbackRoute(input.callbackRoute); + const webOrigins = unique([ + siteUrl, + ...(input.webOrigins ?? []).map((origin) => normalizeWebOrigin(origin, 'webOrigins')), + ]); + const nativeRedirectUris = unique( + (input.nativeRedirectUris ?? []).map((uri) => normalizeNativeRedirectUri(uri)), + ); + + const redirectAllowList = unique([ + ...webOrigins, + ...webOrigins.map((origin) => `${origin}${callbackRoute}`), + ...(input.environment === 'local' ? getLocalAuthRedirectPatterns(callbackRoute) : []), + ...nativeRedirectUris, + ]); + + return { + providerCallbackUrl: `${gatewayOrigin}/auth/v1/callback`, + siteUrl, + redirectAllowList, + serializedRedirectAllowList: redirectAllowList.join(','), + }; +} + +export function getLocalAuthRedirectPatterns(callbackRoute: string): readonly string[] { + const normalizedRoute = normalizeCallbackRoute(callbackRoute); + return [ + `http://127.0.0.1:*${normalizedRoute}`, + `http://localhost:*${normalizedRoute}`, + ]; +} + +export function normalizeAuthCallbackRoute(callbackRoute: string): string { + return normalizeCallbackRoute(callbackRoute); +} + +function normalizeCallbackRoute(callbackRoute: string): string { + const trimmed = callbackRoute.trim(); + if (!trimmed) { + throw new Error('Auth callback route must not be empty.'); + } + + const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`; + const normalized = withLeadingSlash.replace(/\/{2,}/gu, '/'); + if (normalized.includes('?') || normalized.includes('#')) { + throw new Error('Auth callback route must not include a query string or fragment.'); + } + return normalized; +} + +function normalizeWebOrigin(value: string, field: string): string { + const parsed = parseUrl(value, field); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error(`${field} must use http or https.`); + } + if (parsed.username || parsed.password || parsed.search || parsed.hash) { + throw new Error(`${field} must be a canonical origin without credentials, query, or fragment.`); + } + if (parsed.pathname !== '/' && parsed.pathname !== '') { + throw new Error(`${field} must not include a path.`); + } + return parsed.origin; +} + +function normalizeNativeRedirectUri(value: string): string { + const parsed = parseUrl(value, 'nativeRedirectUris'); + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { + throw new Error('nativeRedirectUris must use a non-HTTP application scheme.'); + } + if (parsed.username || parsed.password || parsed.search || parsed.hash) { + throw new Error( + 'nativeRedirectUris must not include credentials, query strings, or fragments.', + ); + } + return parsed.toString(); +} + +function parseUrl(value: string, field: string): URL { + const trimmed = value.trim(); + if (!trimmed) { + throw new Error(`${field} must not contain empty values.`); + } + + try { + return new URL(trimmed); + } catch { + throw new Error(`${field} contains an invalid URL.`); + } +} + +function unique(values: readonly string[]): string[] { + return [...new Set(values)]; +} From efd8ea4cfee599c8665c3d9e4b88bdcfab150684 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:29:03 +0200 Subject: [PATCH 02/18] test(auth): cover redirect environment policy --- src/authRedirects.test.ts | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/authRedirects.test.ts diff --git a/src/authRedirects.test.ts b/src/authRedirects.test.ts new file mode 100644 index 0000000..7bed74d --- /dev/null +++ b/src/authRedirects.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from 'bun:test'; + +import { + getLocalAuthRedirectPatterns, + resolveAuthRedirectConfiguration, +} from './authRedirects'; + +describe('resolveAuthRedirectConfiguration', () => { + test('derives an exact provider callback and local browser callback policy', () => { + const config = resolveAuthRedirectConfiguration({ + environment: 'local', + gatewayOrigin: 'http://127.0.0.1:18081/', + siteOrigin: 'http://127.0.0.1:18080/', + callbackRoute: 'auth/callback', + webOrigins: ['http://localhost:8081'], + nativeRedirectUris: ['ankh-demo://auth/callback'], + }); + + expect(config.providerCallbackUrl).toBe( + 'http://127.0.0.1:18081/auth/v1/callback', + ); + expect(config.siteUrl).toBe('http://127.0.0.1:18080'); + expect(config.redirectAllowList).toEqual([ + 'http://127.0.0.1:18080', + 'http://localhost:8081', + 'http://127.0.0.1:18080/auth/callback', + 'http://localhost:8081/auth/callback', + 'http://127.0.0.1:*/auth/callback', + 'http://localhost:*/auth/callback', + 'ankh-demo://auth/callback', + ]); + }); + + test('never leaks local wildcard policy into preview or production', () => { + for (const environment of ['preview', 'production'] as const) { + const config = resolveAuthRedirectConfiguration({ + environment, + gatewayOrigin: 'https://api.example.test', + siteOrigin: 'https://app.example.test', + callbackRoute: '/auth/callback', + }); + + expect(config.redirectAllowList).toEqual([ + 'https://app.example.test', + 'https://app.example.test/auth/callback', + ]); + expect(config.serializedRedirectAllowList).not.toContain('localhost'); + expect(config.serializedRedirectAllowList).not.toContain('127.0.0.1'); + expect(config.serializedRedirectAllowList).not.toContain('*'); + } + }); + + test('normalizes callback routes and rejects unsafe origins', () => { + expect(getLocalAuthRedirectPatterns('//auth//callback')).toEqual([ + 'http://127.0.0.1:*/auth/callback', + 'http://localhost:*/auth/callback', + ]); + + expect(() => + resolveAuthRedirectConfiguration({ + environment: 'production', + gatewayOrigin: 'https://user:secret@example.test', + siteOrigin: 'https://app.example.test', + callbackRoute: '/auth/callback', + }), + ).toThrow('gatewayOrigin must be a canonical origin'); + }); +}); From 98611df0edb913e9e104b29e8ed311a0c655c83d Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:29:16 +0200 Subject: [PATCH 03/18] feat(auth): export redirect configuration helpers --- src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/index.ts b/src/index.ts index 79d3689..69279d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,14 @@ import type { export type { ApiInfrastructureArtifacts } from './apiArtifacts'; export { generateApiInfrastructureArtifacts } from './apiArtifacts'; +export { + getLocalAuthRedirectPatterns, + normalizeAuthCallbackRoute, + resolveAuthRedirectConfiguration, + type AuthRedirectConfiguration, + type AuthRedirectEnvironment, + type ResolveAuthRedirectConfigurationInput, +} from './authRedirects'; export { createInfraSecretStoreAdapter, type CreateInfraSecretStoreAdapterInput, From 4fc6fc394b876d13db356578c2761504de3d82bf Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:30:28 +0200 Subject: [PATCH 04/18] feat(auth): reconcile GoTrue redirects and rollout --- src/adapters/minikube/auth/supabase/index.ts | 153 ++++++++++++++++++- 1 file changed, 148 insertions(+), 5 deletions(-) diff --git a/src/adapters/minikube/auth/supabase/index.ts b/src/adapters/minikube/auth/supabase/index.ts index c42ad8c..92aa717 100644 --- a/src/adapters/minikube/auth/supabase/index.ts +++ b/src/adapters/minikube/auth/supabase/index.ts @@ -1,11 +1,21 @@ +import { + getLocalAuthRedirectPatterns, + normalizeAuthCallbackRoute, +} from '../../../../authRedirects'; import type { InfraManifestInput } from '../../../../types'; -import type { MinikubeAdapterArtifacts } from '../../contracts'; +import type { MinikubeAdapterArtifacts, MinikubeProviderLifecycle } from '../../contracts'; import { getSupabaseProfileReconciliation, type ResolvedProfileModel, resolveSupabaseProfileModel, } from './profile'; +interface SupabaseOAuthRuntimeModel { + callbackRoute: string; + envPrefixes: readonly string[]; + localRedirectPatterns: readonly string[]; +} + export function generateSupabaseAuthArtifacts(args: { manifest: InfraManifestInput; namespace: string; @@ -21,6 +31,7 @@ export function generateSupabaseAuthArtifacts(args: { const authzKind = manifest.auth?.authorization?.kind ?? 'RBAC'; const authFieldModel = resolveAuthFieldModel(manifest); const profileModel = resolveSupabaseProfileModel(manifest); + const oauthRuntime = resolveOAuthRuntimeModel(manifest); const warnings: string[] = []; if (scope !== 'global') { @@ -63,14 +74,14 @@ export function generateSupabaseAuthArtifacts(args: { : []), { path: `${docsRoot}/supabase-runtime-wiring.md`, - content: getSupabaseRuntimeWiringGuide(profileModel), + content: getSupabaseRuntimeWiringGuide(profileModel, oauthRuntime), }, ], resources: [ `${resourceRoot}/supabase-auth.configmap.yaml`, `${resourceRoot}/app-runtime-auth.env.configmap.yaml`, ], - providerLifecycle: [], + providerLifecycle: oauthRuntime ? [getOAuthProviderLifecycle(oauthRuntime)] : [], envEntries: ['EXPO_PUBLIC_SUPABASE_URL=', 'EXPO_PUBLIC_SUPABASE_ANON_KEY='], warnings, }; @@ -143,7 +154,10 @@ data: `; } -function getSupabaseRuntimeWiringGuide(profileModel: ResolvedProfileModel) { +function getSupabaseRuntimeWiringGuide( + profileModel: ResolvedProfileModel, + oauthRuntime: SupabaseOAuthRuntimeModel | null, +) { const profileSection = profileModel.enabled ? ` ## App Profile Table @@ -158,6 +172,25 @@ The generated profile reconciliation: - allows signed-in users to read and update their own profile row - drops stale Ankhorage-managed profile columns that are no longer configured - creates a trigger for new auth users when \`AUTH_PROFILE_CREATE_STRATEGY=trigger\` +` + : ''; + const oauthSection = oauthRuntime + ? ` +## OAuth Redirect Reconciliation + +The provider callback is always derived from the active project gateway as +\`\${API_EXTERNAL_URL%/}/callback\`, which resolves to the project-owned +\`/auth/v1/callback\` endpoint. GoTrue-to-app redirects use the configured callback route +\`${oauthRuntime.callbackRoute}\`. + +Local Minikube reconciliation adds only callback-scoped loopback patterns: + +${oauthRuntime.localRedirectPatterns.map((pattern) => `- \`${pattern}\``).join('\n')} + +Native callback URIs may be supplied through \`OAUTH_NATIVE_REDIRECT_URLS\`. The generated +provider lifecycle writes only redirect metadata, never OAuth client secrets. It updates the +GoTrue deployment environment, forces \`deployment/auth\` to restart, and waits up to 600 +seconds for the rollout. A failed rollout stops Infra Up before its success message. ` : ''; @@ -216,7 +249,7 @@ with \`optional: true\` for auth sources. - \`SUPABASE_ANON_KEY\` - \`EXPO_PUBLIC_SUPABASE_URL\` - \`EXPO_PUBLIC_SUPABASE_ANON_KEY\` -${profileSection} +${profileSection}${oauthSection} ## Runtime Secrets When Supabase is enabled, \`scripts/up.sh\` creates/updates \`Secret/supabase-public-runtime\` @@ -225,6 +258,116 @@ app \`.env.local\`. Privileged Supabase runtime secrets remain in the \`supabase `; } +function resolveOAuthRuntimeModel(manifest: InfraManifestInput): SupabaseOAuthRuntimeModel | null { + const oauth = manifest.auth?.oauth; + if (!oauth?.enabled) return null; + + const providers = oauth.providers.filter((provider) => provider.enabled !== false); + if (providers.length === 0) return null; + + const callbackRoute = normalizeAuthCallbackRoute(oauth.callbackRoute ?? '/auth/callback'); + const envPrefixes = providers.map((provider) => { + const prefix = provider.id + .trim() + .toUpperCase() + .replace(/[^A-Z0-9]+/gu, '_') + .replace(/^_+|_+$/gu, ''); + if (!prefix) { + throw new Error(`OAuth provider "${provider.id}" cannot be mapped to GoTrue environment keys.`); + } + return prefix; + }); + + return { + callbackRoute, + envPrefixes, + localRedirectPatterns: getLocalAuthRedirectPatterns(callbackRoute), + }; +} + +function getOAuthProviderLifecycle( + oauthRuntime: SupabaseOAuthRuntimeModel, +): MinikubeProviderLifecycle { + return { + id: 'supabase-auth', + namespace: 'supabase', + endpoints: [], + readinessChecks: [ + { + label: 'GoTrue', + namespace: 'supabase', + resource: 'deployment/auth', + timeoutSeconds: 600, + }, + ], + migrationCommands: [], + reconciliationCommands: [ + { + label: 'OAuth redirect and runtime rollout reconciliation', + command: getOAuthRuntimeReconciliationCommand(oauthRuntime), + }, + ], + statusChecks: [ + { + label: 'OAuth redirect configuration', + command: getOAuthRuntimeStatusCommand(oauthRuntime), + }, + ], + }; +} + +function getOAuthRuntimeReconciliationCommand(oauthRuntime: SupabaseOAuthRuntimeModel): string { + const localPatterns = oauthRuntime.localRedirectPatterns.join(','); + const providerRedirectAssignments = oauthRuntime.envPrefixes + .map( + (prefix) => + `GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI="\${oauth_provider_callback}"`, + ) + .join(' \\\n '); + const providerExports = oauthRuntime.envPrefixes + .map( + (prefix) => `export GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI="\${oauth_provider_callback}" +write_env_value GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI "\${oauth_provider_callback}"`, + ) + .join('\n'); + + return `oauth_callback_route='${oauthRuntime.callbackRoute}' +oauth_callback_path="\${oauth_callback_route#/}" +oauth_site_url="\${SITE_URL%/}" +oauth_provider_callback="\${API_EXTERNAL_URL%/}/callback" +oauth_redirect_allow_list="\${oauth_site_url},\${oauth_site_url}/\${oauth_callback_path},${localPatterns}" +if [[ -n "\${OAUTH_NATIVE_REDIRECT_URLS:-}" ]]; then + oauth_redirect_allow_list="\${oauth_redirect_allow_list},\${OAUTH_NATIVE_REDIRECT_URLS}" +fi +export ADDITIONAL_REDIRECT_URLS="\${oauth_redirect_allow_list}" +write_env_value ADDITIONAL_REDIRECT_URLS "\${oauth_redirect_allow_list}" +${providerExports} +kubectl --context "\${PROFILE}" -n supabase set env deployment/auth \\ + API_EXTERNAL_URL="\${API_EXTERNAL_URL}" \\ + GOTRUE_SITE_URL="\${SITE_URL}" \\ + GOTRUE_URI_ALLOW_LIST="\${oauth_redirect_allow_list}" \\ + GOTRUE_JWT_ISSUER="\${API_EXTERNAL_URL}" \\ + ${providerRedirectAssignments} >/dev/null +kubectl --context "\${PROFILE}" -n supabase rollout restart deployment/auth >/dev/null +kubectl --context "\${PROFILE}" -n supabase rollout status deployment/auth --timeout=600s`; +} + +function getOAuthRuntimeStatusCommand(oauthRuntime: SupabaseOAuthRuntimeModel): string { + return `oauth_callback_route='${oauthRuntime.callbackRoute}' +oauth_callback_path="\${oauth_callback_route#/}" +if [[ -n "\${API_EXTERNAL_URL:-}" ]]; then + echo "- provider supabase-auth/provider-callback: \${API_EXTERNAL_URL%/}/callback" +else + echo "- provider supabase-auth/provider-callback: unavailable" +fi +if [[ -n "\${SITE_URL:-}" ]]; then + echo "- provider supabase-auth/app-callback: \${SITE_URL%/}/\${oauth_callback_path}" +else + echo "- provider supabase-auth/app-callback: unavailable" +fi +echo "- provider supabase-auth/local-callback-patterns: ${oauthRuntime.localRedirectPatterns.join(',')}"`; +} + const DEFAULT_SIGN_IN_IDENTIFIERS = ['email']; const DEFAULT_SIGN_UP_REQUIRED_FIELDS = ['email', 'password']; const DEFAULT_SIGN_UP_OPTIONAL_FIELDS = ['firstName', 'lastName']; From fab2b9e8f89fac8a2b1d9e208fd5345141a5e575 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:30:56 +0200 Subject: [PATCH 05/18] fix(minikube): keep built-in namespaces canonical --- src/adapters/minikube/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/adapters/minikube/index.ts b/src/adapters/minikube/index.ts index c0f76e6..14b24b5 100644 --- a/src/adapters/minikube/index.ts +++ b/src/adapters/minikube/index.ts @@ -12,6 +12,7 @@ import { generateSecretStoreArtifacts } from './secrets'; import { generateStorageArtifacts } from './storage'; const CANONICAL_PROJECT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/; +const BUILT_IN_NAMESPACES = new Set([APP_NAMESPACE, 'supabase']); export function generateMinikubeInfra( manifest: InfraManifestInput, @@ -59,7 +60,7 @@ export function generateMinikubeInfra( ]; const providerNamespaces = unique([ ...providerLifecycle.map((contribution) => contribution.namespace), - ]); + ]).filter((providerNamespace) => !BUILT_IN_NAMESPACES.has(providerNamespace)); const extraEnvEntries = unique([ ...authArtifacts.envEntries, ...authzArtifacts.envEntries, From 87033ae40de1db95926ffb39c097347c7abec710 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:31:30 +0200 Subject: [PATCH 06/18] test(auth): cover GoTrue redirect reconciliation --- .../auth/supabase/oauthRuntime.test.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/adapters/minikube/auth/supabase/oauthRuntime.test.ts diff --git a/src/adapters/minikube/auth/supabase/oauthRuntime.test.ts b/src/adapters/minikube/auth/supabase/oauthRuntime.test.ts new file mode 100644 index 0000000..4e889b5 --- /dev/null +++ b/src/adapters/minikube/auth/supabase/oauthRuntime.test.ts @@ -0,0 +1,145 @@ +import type { InfraManifest } from '@ankhorage/contracts'; +import { describe, expect, test } from 'bun:test'; + +import { generateInfrastructure } from '../../../../index'; +import { createAppManifest } from '../../../../testSupport'; +import { generateSupabaseAuthArtifacts } from './index'; + +describe('Supabase OAuth runtime reconciliation', () => { + test('contributes callback-scoped local redirects and a bounded GoTrue restart', () => { + const artifacts = generateSupabaseAuthArtifacts({ + namespace: 'app', + manifest: createOAuthManifest(), + }); + const lifecycle = artifacts.providerLifecycle[0]; + const command = lifecycle?.reconciliationCommands[0]?.command ?? ''; + const statusCommand = lifecycle?.statusChecks[0]?.command ?? ''; + const guide = getFile( + artifacts.files, + 'infra/minikube/auth/supabase-runtime-wiring.md', + ); + + expect(lifecycle?.id).toBe('supabase-auth'); + expect(lifecycle?.namespace).toBe('supabase'); + expect(lifecycle?.readinessChecks).toEqual([ + { + label: 'GoTrue', + namespace: 'supabase', + resource: 'deployment/auth', + timeoutSeconds: 600, + }, + ]); + expect(command).toContain('oauth_provider_callback="${API_EXTERNAL_URL%/}/callback"'); + expect(command).toContain('http://127.0.0.1:*/auth/callback'); + expect(command).toContain('http://localhost:*/auth/callback'); + expect(command).toContain('OAUTH_NATIVE_REDIRECT_URLS'); + expect(command).toContain( + 'GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="${oauth_provider_callback}"', + ); + expect(command).toContain('GOTRUE_URI_ALLOW_LIST="${oauth_redirect_allow_list}"'); + expect(command).toContain('rollout restart deployment/auth'); + expect(command).toContain('rollout status deployment/auth --timeout=600s'); + expect(command.indexOf('set env deployment/auth')).toBeLessThan( + command.indexOf('rollout restart deployment/auth'), + ); + expect(command.indexOf('rollout restart deployment/auth')).toBeLessThan( + command.indexOf('rollout status deployment/auth'), + ); + expect(command).not.toContain('clientSecret'); + expect(command).not.toContain('GOTRUE_EXTERNAL_GOOGLE_SECRET='); + expect(statusCommand).toContain('provider supabase-auth/provider-callback'); + expect(statusCommand).toContain('provider supabase-auth/app-callback'); + expect(guide).toContain('OAuth Redirect Reconciliation'); + expect(guide).toContain('A failed rollout stops Infra Up before its success message.'); + }); + + test('integrates reconciliation after generated runtime resources without duplicate namespaces', () => { + const result = generateInfrastructure(createOAuthManifest(), { + appManifest: createAppManifest('oauth-app'), + }); + const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh'); + const kustomization = getFile(result.files, 'infra/minikube/k8s/kustomization.yaml'); + const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh'); + + expect(kustomization.match(/namespaces\/supabase\.yaml/gu)).toHaveLength(1); + expect(upScript).toContain('Running provider supabase-auth OAuth redirect and runtime rollout reconciliation.'); + expect(upScript).toContain('kubectl --context "${PROFILE}" -n supabase set env deployment/auth'); + expect(upScript).toContain('kubectl --context "${PROFILE}" -n supabase rollout restart deployment/auth'); + expect(upScript).toContain( + 'kubectl --context "${PROFILE}" -n supabase rollout status deployment/auth --timeout=600s', + ); + expect(upScript.lastIndexOf('run_provider_reconciliation')).toBeLessThan( + upScript.lastIndexOf("echo \"Minikube infrastructure for '${PROFILE}' is running.\""), + ); + expect(upScript.indexOf('credentialsRef auth/oauth/google')).toBeLessThan( + upScript.lastIndexOf('run_provider_reconciliation'), + ); + expect(statusScript).toContain('provider supabase-auth/local-callback-patterns'); + }); + + test('keeps canonical port groups distinct for concurrent projects', () => { + const first = generateInfrastructure(createOAuthManifest(), { + appManifest: createAppManifest('oauth-one'), + }); + const second = generateInfrastructure(createOAuthManifest(), { + appManifest: createAppManifest('oauth-two'), + }); + + const firstEnv = getFile(first.files, 'infra/minikube/.env.example'); + const secondEnv = getFile(second.files, 'infra/minikube/.env.example'); + const firstGatewayPort = readEnvValue(firstEnv, 'SUPABASE_GATEWAY_FORWARD_LOCAL_PORT'); + const secondGatewayPort = readEnvValue(secondEnv, 'SUPABASE_GATEWAY_FORWARD_LOCAL_PORT'); + const firstAppPort = readEnvValue(firstEnv, 'APP_PORT_FORWARD_LOCAL_PORT'); + const secondAppPort = readEnvValue(secondEnv, 'APP_PORT_FORWARD_LOCAL_PORT'); + + expect(firstGatewayPort).not.toBe(secondGatewayPort); + expect(firstAppPort).not.toBe(secondAppPort); + expect(`http://127.0.0.1:${firstGatewayPort}/auth/v1/callback`).not.toBe( + `http://127.0.0.1:${secondGatewayPort}/auth/v1/callback`, + ); + }); +}); + +function createOAuthManifest(): InfraManifest { + return { + deployment: { + target: 'minikube', + monitoring: false, + }, + auth: { + scope: 'global', + provider: 'supabase', + oauth: { + enabled: true, + callbackRoute: '/auth/callback', + providers: [ + { + id: 'google', + enabled: true, + credentialsRef: 'auth/oauth/google', + }, + ], + }, + }, + database: { + provider: 'supabase', + tier: 'dev', + }, + secretStore: { + provider: 'supabase-vault', + }, + plugins: [], + }; +} + +function getFile(files: readonly { path: string; content: string }[], path: string): string { + const file = files.find((candidate) => candidate.path === path); + if (!file) throw new Error(`Missing generated file: ${path}`); + return file.content; +} + +function readEnvValue(content: string, key: string): string { + const line = content.split('\n').find((candidate) => candidate.startsWith(`${key}=`)); + if (!line) throw new Error(`Missing generated env key: ${key}`); + return line.slice(key.length + 1); +} From 4fa2b577fe44a3044b86d4e5ed4f8189b5ff247f Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:31:39 +0200 Subject: [PATCH 07/18] chore: add auth redirect reconciliation changeset --- .changeset/bright-auth-rollouts.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-auth-rollouts.md diff --git a/.changeset/bright-auth-rollouts.md b/.changeset/bright-auth-rollouts.md new file mode 100644 index 0000000..60e0e7b --- /dev/null +++ b/.changeset/bright-auth-rollouts.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/infra': minor +--- + +Add canonical environment-aware Auth redirect configuration and reconcile Minikube GoTrue callback settings through a forced, bounded deployment rollout. From 0328c5043ee4a9189185a09d57f213efc777f391 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:32:53 +0200 Subject: [PATCH 08/18] test(auth): cover multiple provider rollout assignments --- .../auth/supabase/providerAssignments.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/adapters/minikube/auth/supabase/providerAssignments.test.ts diff --git a/src/adapters/minikube/auth/supabase/providerAssignments.test.ts b/src/adapters/minikube/auth/supabase/providerAssignments.test.ts new file mode 100644 index 0000000..f83d81f --- /dev/null +++ b/src/adapters/minikube/auth/supabase/providerAssignments.test.ts @@ -0,0 +1,40 @@ +import type { InfraManifest } from '@ankhorage/contracts'; +import { expect, test } from 'bun:test'; + +import { generateInfrastructure } from '../../../../index'; +import { createAppManifest } from '../../../../testSupport'; + +test('keeps multiple provider redirect assignments as separate shell arguments', () => { + const manifest: InfraManifest = { + deployment: { target: 'minikube', monitoring: false }, + auth: { + scope: 'global', + provider: 'supabase', + oauth: { + enabled: true, + callbackRoute: '/auth/callback', + providers: [ + { id: 'google', enabled: true, credentialsRef: 'auth/oauth/google' }, + { id: 'apple', enabled: true, credentialsRef: 'auth/oauth/apple' }, + ], + }, + }, + database: { provider: 'supabase', tier: 'dev' }, + secretStore: { provider: 'supabase-vault' }, + plugins: [], + }; + const result = generateInfrastructure(manifest, { + appManifest: createAppManifest('multi-oauth'), + }); + const upScript = result.files.find( + (file) => file.path === 'infra/minikube/scripts/up.sh', + )?.content; + + expect(upScript).toContain( + 'GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="${oauth_provider_callback}"', + ); + expect(upScript).toContain( + 'GOTRUE_EXTERNAL_APPLE_REDIRECT_URI="${oauth_provider_callback}"', + ); + expect(upScript).not.toContain('\\ GOTRUE_EXTERNAL_APPLE_REDIRECT_URI'); +}); From 1b31bc9ce28cef0c00dbf3b5d4b4aedaabc2dfb4 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:34:42 +0200 Subject: [PATCH 09/18] refactor(auth): isolate OAuth runtime lifecycle --- src/adapters/minikube/auth/supabase/index.ts | 153 +------------------ 1 file changed, 5 insertions(+), 148 deletions(-) diff --git a/src/adapters/minikube/auth/supabase/index.ts b/src/adapters/minikube/auth/supabase/index.ts index 92aa717..c42ad8c 100644 --- a/src/adapters/minikube/auth/supabase/index.ts +++ b/src/adapters/minikube/auth/supabase/index.ts @@ -1,21 +1,11 @@ -import { - getLocalAuthRedirectPatterns, - normalizeAuthCallbackRoute, -} from '../../../../authRedirects'; import type { InfraManifestInput } from '../../../../types'; -import type { MinikubeAdapterArtifacts, MinikubeProviderLifecycle } from '../../contracts'; +import type { MinikubeAdapterArtifacts } from '../../contracts'; import { getSupabaseProfileReconciliation, type ResolvedProfileModel, resolveSupabaseProfileModel, } from './profile'; -interface SupabaseOAuthRuntimeModel { - callbackRoute: string; - envPrefixes: readonly string[]; - localRedirectPatterns: readonly string[]; -} - export function generateSupabaseAuthArtifacts(args: { manifest: InfraManifestInput; namespace: string; @@ -31,7 +21,6 @@ export function generateSupabaseAuthArtifacts(args: { const authzKind = manifest.auth?.authorization?.kind ?? 'RBAC'; const authFieldModel = resolveAuthFieldModel(manifest); const profileModel = resolveSupabaseProfileModel(manifest); - const oauthRuntime = resolveOAuthRuntimeModel(manifest); const warnings: string[] = []; if (scope !== 'global') { @@ -74,14 +63,14 @@ export function generateSupabaseAuthArtifacts(args: { : []), { path: `${docsRoot}/supabase-runtime-wiring.md`, - content: getSupabaseRuntimeWiringGuide(profileModel, oauthRuntime), + content: getSupabaseRuntimeWiringGuide(profileModel), }, ], resources: [ `${resourceRoot}/supabase-auth.configmap.yaml`, `${resourceRoot}/app-runtime-auth.env.configmap.yaml`, ], - providerLifecycle: oauthRuntime ? [getOAuthProviderLifecycle(oauthRuntime)] : [], + providerLifecycle: [], envEntries: ['EXPO_PUBLIC_SUPABASE_URL=', 'EXPO_PUBLIC_SUPABASE_ANON_KEY='], warnings, }; @@ -154,10 +143,7 @@ data: `; } -function getSupabaseRuntimeWiringGuide( - profileModel: ResolvedProfileModel, - oauthRuntime: SupabaseOAuthRuntimeModel | null, -) { +function getSupabaseRuntimeWiringGuide(profileModel: ResolvedProfileModel) { const profileSection = profileModel.enabled ? ` ## App Profile Table @@ -172,25 +158,6 @@ The generated profile reconciliation: - allows signed-in users to read and update their own profile row - drops stale Ankhorage-managed profile columns that are no longer configured - creates a trigger for new auth users when \`AUTH_PROFILE_CREATE_STRATEGY=trigger\` -` - : ''; - const oauthSection = oauthRuntime - ? ` -## OAuth Redirect Reconciliation - -The provider callback is always derived from the active project gateway as -\`\${API_EXTERNAL_URL%/}/callback\`, which resolves to the project-owned -\`/auth/v1/callback\` endpoint. GoTrue-to-app redirects use the configured callback route -\`${oauthRuntime.callbackRoute}\`. - -Local Minikube reconciliation adds only callback-scoped loopback patterns: - -${oauthRuntime.localRedirectPatterns.map((pattern) => `- \`${pattern}\``).join('\n')} - -Native callback URIs may be supplied through \`OAUTH_NATIVE_REDIRECT_URLS\`. The generated -provider lifecycle writes only redirect metadata, never OAuth client secrets. It updates the -GoTrue deployment environment, forces \`deployment/auth\` to restart, and waits up to 600 -seconds for the rollout. A failed rollout stops Infra Up before its success message. ` : ''; @@ -249,7 +216,7 @@ with \`optional: true\` for auth sources. - \`SUPABASE_ANON_KEY\` - \`EXPO_PUBLIC_SUPABASE_URL\` - \`EXPO_PUBLIC_SUPABASE_ANON_KEY\` -${profileSection}${oauthSection} +${profileSection} ## Runtime Secrets When Supabase is enabled, \`scripts/up.sh\` creates/updates \`Secret/supabase-public-runtime\` @@ -258,116 +225,6 @@ app \`.env.local\`. Privileged Supabase runtime secrets remain in the \`supabase `; } -function resolveOAuthRuntimeModel(manifest: InfraManifestInput): SupabaseOAuthRuntimeModel | null { - const oauth = manifest.auth?.oauth; - if (!oauth?.enabled) return null; - - const providers = oauth.providers.filter((provider) => provider.enabled !== false); - if (providers.length === 0) return null; - - const callbackRoute = normalizeAuthCallbackRoute(oauth.callbackRoute ?? '/auth/callback'); - const envPrefixes = providers.map((provider) => { - const prefix = provider.id - .trim() - .toUpperCase() - .replace(/[^A-Z0-9]+/gu, '_') - .replace(/^_+|_+$/gu, ''); - if (!prefix) { - throw new Error(`OAuth provider "${provider.id}" cannot be mapped to GoTrue environment keys.`); - } - return prefix; - }); - - return { - callbackRoute, - envPrefixes, - localRedirectPatterns: getLocalAuthRedirectPatterns(callbackRoute), - }; -} - -function getOAuthProviderLifecycle( - oauthRuntime: SupabaseOAuthRuntimeModel, -): MinikubeProviderLifecycle { - return { - id: 'supabase-auth', - namespace: 'supabase', - endpoints: [], - readinessChecks: [ - { - label: 'GoTrue', - namespace: 'supabase', - resource: 'deployment/auth', - timeoutSeconds: 600, - }, - ], - migrationCommands: [], - reconciliationCommands: [ - { - label: 'OAuth redirect and runtime rollout reconciliation', - command: getOAuthRuntimeReconciliationCommand(oauthRuntime), - }, - ], - statusChecks: [ - { - label: 'OAuth redirect configuration', - command: getOAuthRuntimeStatusCommand(oauthRuntime), - }, - ], - }; -} - -function getOAuthRuntimeReconciliationCommand(oauthRuntime: SupabaseOAuthRuntimeModel): string { - const localPatterns = oauthRuntime.localRedirectPatterns.join(','); - const providerRedirectAssignments = oauthRuntime.envPrefixes - .map( - (prefix) => - `GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI="\${oauth_provider_callback}"`, - ) - .join(' \\\n '); - const providerExports = oauthRuntime.envPrefixes - .map( - (prefix) => `export GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI="\${oauth_provider_callback}" -write_env_value GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI "\${oauth_provider_callback}"`, - ) - .join('\n'); - - return `oauth_callback_route='${oauthRuntime.callbackRoute}' -oauth_callback_path="\${oauth_callback_route#/}" -oauth_site_url="\${SITE_URL%/}" -oauth_provider_callback="\${API_EXTERNAL_URL%/}/callback" -oauth_redirect_allow_list="\${oauth_site_url},\${oauth_site_url}/\${oauth_callback_path},${localPatterns}" -if [[ -n "\${OAUTH_NATIVE_REDIRECT_URLS:-}" ]]; then - oauth_redirect_allow_list="\${oauth_redirect_allow_list},\${OAUTH_NATIVE_REDIRECT_URLS}" -fi -export ADDITIONAL_REDIRECT_URLS="\${oauth_redirect_allow_list}" -write_env_value ADDITIONAL_REDIRECT_URLS "\${oauth_redirect_allow_list}" -${providerExports} -kubectl --context "\${PROFILE}" -n supabase set env deployment/auth \\ - API_EXTERNAL_URL="\${API_EXTERNAL_URL}" \\ - GOTRUE_SITE_URL="\${SITE_URL}" \\ - GOTRUE_URI_ALLOW_LIST="\${oauth_redirect_allow_list}" \\ - GOTRUE_JWT_ISSUER="\${API_EXTERNAL_URL}" \\ - ${providerRedirectAssignments} >/dev/null -kubectl --context "\${PROFILE}" -n supabase rollout restart deployment/auth >/dev/null -kubectl --context "\${PROFILE}" -n supabase rollout status deployment/auth --timeout=600s`; -} - -function getOAuthRuntimeStatusCommand(oauthRuntime: SupabaseOAuthRuntimeModel): string { - return `oauth_callback_route='${oauthRuntime.callbackRoute}' -oauth_callback_path="\${oauth_callback_route#/}" -if [[ -n "\${API_EXTERNAL_URL:-}" ]]; then - echo "- provider supabase-auth/provider-callback: \${API_EXTERNAL_URL%/}/callback" -else - echo "- provider supabase-auth/provider-callback: unavailable" -fi -if [[ -n "\${SITE_URL:-}" ]]; then - echo "- provider supabase-auth/app-callback: \${SITE_URL%/}/\${oauth_callback_path}" -else - echo "- provider supabase-auth/app-callback: unavailable" -fi -echo "- provider supabase-auth/local-callback-patterns: ${oauthRuntime.localRedirectPatterns.join(',')}"`; -} - const DEFAULT_SIGN_IN_IDENTIFIERS = ['email']; const DEFAULT_SIGN_UP_REQUIRED_FIELDS = ['email', 'password']; const DEFAULT_SIGN_UP_OPTIONAL_FIELDS = ['firstName', 'lastName']; From 3c95cc601b3b79d81600ec0cb45b32cca90e6b2a Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:35:17 +0200 Subject: [PATCH 10/18] feat(auth): add GoTrue OAuth runtime lifecycle --- src/adapters/minikube/auth/oauthRuntime.ts | 172 +++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/adapters/minikube/auth/oauthRuntime.ts diff --git a/src/adapters/minikube/auth/oauthRuntime.ts b/src/adapters/minikube/auth/oauthRuntime.ts new file mode 100644 index 0000000..0d93de0 --- /dev/null +++ b/src/adapters/minikube/auth/oauthRuntime.ts @@ -0,0 +1,172 @@ +import { + getLocalAuthRedirectPatterns, + normalizeAuthCallbackRoute, +} from '../../../authRedirects'; +import type { InfraManifestInput } from '../../../types'; +import type { MinikubeAdapterArtifacts, MinikubeProviderLifecycle } from '../contracts'; + +interface SupabaseOAuthRuntimeModel { + callbackRoute: string; + envPrefixes: readonly string[]; + localRedirectPatterns: readonly string[]; +} + +export function generateSupabaseOAuthRuntimeArtifacts( + manifest: InfraManifestInput, +): MinikubeAdapterArtifacts { + const oauthRuntime = resolveOAuthRuntimeModel(manifest); + if (!oauthRuntime) { + return { + files: [], + resources: [], + providerLifecycle: [], + envEntries: [], + warnings: [], + }; + } + + return { + files: [ + { + path: 'infra/minikube/auth/oauth-runtime.md', + content: getOAuthRuntimeGuide(oauthRuntime), + }, + ], + resources: [], + providerLifecycle: [getOAuthProviderLifecycle(oauthRuntime)], + envEntries: [], + warnings: [], + }; +} + +function resolveOAuthRuntimeModel(manifest: InfraManifestInput): SupabaseOAuthRuntimeModel | null { + const oauth = manifest.auth?.oauth; + if (!oauth?.enabled) return null; + + const providers = oauth.providers.filter((provider) => provider.enabled !== false); + if (providers.length === 0) return null; + + const callbackRoute = normalizeAuthCallbackRoute(oauth.callbackRoute); + const envPrefixes = providers.map((provider) => { + const prefix = provider.id + .trim() + .toUpperCase() + .replace(/[^A-Z0-9]+/gu, '_') + .replace(/^_+|_+$/gu, ''); + if (!prefix) { + throw new Error( + `OAuth provider "${provider.id}" cannot be mapped to GoTrue environment keys.`, + ); + } + return prefix; + }); + + return { + callbackRoute, + envPrefixes, + localRedirectPatterns: getLocalAuthRedirectPatterns(callbackRoute), + }; +} + +function getOAuthProviderLifecycle( + oauthRuntime: SupabaseOAuthRuntimeModel, +): MinikubeProviderLifecycle { + return { + id: 'supabase-auth', + namespace: 'supabase', + endpoints: [], + readinessChecks: [ + { + label: 'GoTrue', + namespace: 'supabase', + resource: 'deployment/auth', + timeoutSeconds: 600, + }, + ], + migrationCommands: [], + reconciliationCommands: [ + { + label: 'OAuth redirect and runtime rollout reconciliation', + command: getOAuthRuntimeReconciliationCommand(oauthRuntime), + }, + ], + statusChecks: [ + { + label: 'OAuth redirect configuration', + command: getOAuthRuntimeStatusCommand(oauthRuntime), + }, + ], + }; +} + +function getOAuthRuntimeReconciliationCommand(oauthRuntime: SupabaseOAuthRuntimeModel): string { + const localPatterns = oauthRuntime.localRedirectPatterns.join(','); + const providerRedirectAssignments = oauthRuntime.envPrefixes + .map((prefix) => `GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI="\${oauth_provider_callback}"`) + .join(' '); + const providerExports = oauthRuntime.envPrefixes + .map( + (prefix) => `export GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI="\${oauth_provider_callback}" +write_env_value GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI "\${oauth_provider_callback}"`, + ) + .join('\n'); + + return `oauth_callback_route='${oauthRuntime.callbackRoute}' +oauth_callback_path="\${oauth_callback_route#/}" +oauth_site_url="\${SITE_URL%/}" +oauth_provider_callback="\${API_EXTERNAL_URL%/}/callback" +oauth_redirect_allow_list="\${oauth_site_url},\${oauth_site_url}/\${oauth_callback_path},${localPatterns}" +if [[ -n "\${OAUTH_NATIVE_REDIRECT_URLS:-}" ]]; then + oauth_redirect_allow_list="\${oauth_redirect_allow_list},\${OAUTH_NATIVE_REDIRECT_URLS}" +fi +export ADDITIONAL_REDIRECT_URLS="\${oauth_redirect_allow_list}" +write_env_value ADDITIONAL_REDIRECT_URLS "\${oauth_redirect_allow_list}" +${providerExports} +kubectl --context "\${PROFILE}" -n supabase set env deployment/auth \\ + API_EXTERNAL_URL="\${API_EXTERNAL_URL}" \\ + GOTRUE_SITE_URL="\${SITE_URL}" \\ + GOTRUE_URI_ALLOW_LIST="\${oauth_redirect_allow_list}" \\ + GOTRUE_JWT_ISSUER="\${API_EXTERNAL_URL}" \\ + ${providerRedirectAssignments} >/dev/null +kubectl --context "\${PROFILE}" -n supabase rollout restart deployment/auth >/dev/null +kubectl --context "\${PROFILE}" -n supabase rollout status deployment/auth --timeout=600s`; +} + +function getOAuthRuntimeStatusCommand(oauthRuntime: SupabaseOAuthRuntimeModel): string { + return `oauth_callback_route='${oauthRuntime.callbackRoute}' +oauth_callback_path="\${oauth_callback_route#/}" +if [[ -n "\${API_EXTERNAL_URL:-}" ]]; then + echo "- provider supabase-auth/provider-callback: \${API_EXTERNAL_URL%/}/callback" +else + echo "- provider supabase-auth/provider-callback: unavailable" +fi +if [[ -n "\${SITE_URL:-}" ]]; then + echo "- provider supabase-auth/app-callback: \${SITE_URL%/}/\${oauth_callback_path}" +else + echo "- provider supabase-auth/app-callback: unavailable" +fi +echo "- provider supabase-auth/local-callback-patterns: ${oauthRuntime.localRedirectPatterns.join(',')}"`; +} + +function getOAuthRuntimeGuide(oauthRuntime: SupabaseOAuthRuntimeModel): string { + const localPatterns = oauthRuntime.localRedirectPatterns + .map((pattern) => `- \`${pattern}\``) + .join('\n'); + + return `# Supabase OAuth Runtime Reconciliation + +The provider callback is derived from the active project gateway as +\`\${API_EXTERNAL_URL%/}/callback\`, which resolves to the project-owned +\`/auth/v1/callback\` endpoint. GoTrue-to-app redirects use the configured callback route +\`${oauthRuntime.callbackRoute}\`. + +Local Minikube reconciliation adds only callback-scoped loopback patterns: + +${localPatterns} + +Native callback URIs may be supplied through \`OAUTH_NATIVE_REDIRECT_URLS\`. The generated +provider lifecycle writes only redirect metadata, never OAuth client secrets. It updates the +GoTrue deployment environment, forces \`deployment/auth\` to restart, and waits up to 600 +seconds for the rollout. A failed rollout stops Infra Up before its success message. +`; +} From 0b0a5ef1ebe41984287d72a1e2fe63ede5b87950 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:35:31 +0200 Subject: [PATCH 11/18] feat(auth): compose OAuth runtime artifacts --- src/adapters/minikube/auth/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/adapters/minikube/auth/index.ts b/src/adapters/minikube/auth/index.ts index 0a41b5f..d851ed9 100644 --- a/src/adapters/minikube/auth/index.ts +++ b/src/adapters/minikube/auth/index.ts @@ -1,5 +1,6 @@ import type { InfraManifestInput } from '../../../types'; import { emptyMinikubeArtifacts, type MinikubeAdapterArtifacts } from '../contracts'; +import { generateSupabaseOAuthRuntimeArtifacts } from './oauthRuntime'; import { generateSupabaseAuthArtifacts } from './supabase'; export function generateAuthProviderArtifacts(args: { @@ -19,5 +20,17 @@ export function generateAuthProviderArtifacts(args: { ); } - return generateSupabaseAuthArtifacts({ manifest, namespace }); + const providerArtifacts = generateSupabaseAuthArtifacts({ manifest, namespace }); + const oauthArtifacts = generateSupabaseOAuthRuntimeArtifacts(manifest); + + return { + files: [...providerArtifacts.files, ...oauthArtifacts.files], + resources: [...providerArtifacts.resources, ...oauthArtifacts.resources], + providerLifecycle: [ + ...providerArtifacts.providerLifecycle, + ...oauthArtifacts.providerLifecycle, + ], + envEntries: [...providerArtifacts.envEntries, ...oauthArtifacts.envEntries], + warnings: [...providerArtifacts.warnings, ...oauthArtifacts.warnings], + }; } From 16c12045d7ec48735862ea673a0ef2fd467c4fd5 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:36:37 +0200 Subject: [PATCH 12/18] test(auth): align OAuth lifecycle coverage --- .../auth/supabase/oauthRuntime.test.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/adapters/minikube/auth/supabase/oauthRuntime.test.ts b/src/adapters/minikube/auth/supabase/oauthRuntime.test.ts index 4e889b5..8676eaf 100644 --- a/src/adapters/minikube/auth/supabase/oauthRuntime.test.ts +++ b/src/adapters/minikube/auth/supabase/oauthRuntime.test.ts @@ -3,21 +3,15 @@ import { describe, expect, test } from 'bun:test'; import { generateInfrastructure } from '../../../../index'; import { createAppManifest } from '../../../../testSupport'; -import { generateSupabaseAuthArtifacts } from './index'; +import { generateSupabaseOAuthRuntimeArtifacts } from '../oauthRuntime'; describe('Supabase OAuth runtime reconciliation', () => { test('contributes callback-scoped local redirects and a bounded GoTrue restart', () => { - const artifacts = generateSupabaseAuthArtifacts({ - namespace: 'app', - manifest: createOAuthManifest(), - }); - const lifecycle = artifacts.providerLifecycle[0]; + const artifacts = generateSupabaseOAuthRuntimeArtifacts(createOAuthManifest()); + const [lifecycle] = artifacts.providerLifecycle; const command = lifecycle?.reconciliationCommands[0]?.command ?? ''; const statusCommand = lifecycle?.statusChecks[0]?.command ?? ''; - const guide = getFile( - artifacts.files, - 'infra/minikube/auth/supabase-runtime-wiring.md', - ); + const guide = getFile(artifacts.files, 'infra/minikube/auth/oauth-runtime.md'); expect(lifecycle?.id).toBe('supabase-auth'); expect(lifecycle?.namespace).toBe('supabase'); @@ -33,9 +27,7 @@ describe('Supabase OAuth runtime reconciliation', () => { expect(command).toContain('http://127.0.0.1:*/auth/callback'); expect(command).toContain('http://localhost:*/auth/callback'); expect(command).toContain('OAUTH_NATIVE_REDIRECT_URLS'); - expect(command).toContain( - 'GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="${oauth_provider_callback}"', - ); + expect(command).toContain('GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="${oauth_provider_callback}"'); expect(command).toContain('GOTRUE_URI_ALLOW_LIST="${oauth_redirect_allow_list}"'); expect(command).toContain('rollout restart deployment/auth'); expect(command).toContain('rollout status deployment/auth --timeout=600s'); @@ -49,7 +41,7 @@ describe('Supabase OAuth runtime reconciliation', () => { expect(command).not.toContain('GOTRUE_EXTERNAL_GOOGLE_SECRET='); expect(statusCommand).toContain('provider supabase-auth/provider-callback'); expect(statusCommand).toContain('provider supabase-auth/app-callback'); - expect(guide).toContain('OAuth Redirect Reconciliation'); + expect(guide).toContain('Supabase OAuth Runtime Reconciliation'); expect(guide).toContain('A failed rollout stops Infra Up before its success message.'); }); @@ -62,14 +54,20 @@ describe('Supabase OAuth runtime reconciliation', () => { const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh'); expect(kustomization.match(/namespaces\/supabase\.yaml/gu)).toHaveLength(1); - expect(upScript).toContain('Running provider supabase-auth OAuth redirect and runtime rollout reconciliation.'); - expect(upScript).toContain('kubectl --context "${PROFILE}" -n supabase set env deployment/auth'); - expect(upScript).toContain('kubectl --context "${PROFILE}" -n supabase rollout restart deployment/auth'); + expect(upScript).toContain( + 'Running provider supabase-auth OAuth redirect and runtime rollout reconciliation.', + ); + expect(upScript).toContain( + 'kubectl --context "${PROFILE}" -n supabase set env deployment/auth', + ); + expect(upScript).toContain( + 'kubectl --context "${PROFILE}" -n supabase rollout restart deployment/auth', + ); expect(upScript).toContain( 'kubectl --context "${PROFILE}" -n supabase rollout status deployment/auth --timeout=600s', ); expect(upScript.lastIndexOf('run_provider_reconciliation')).toBeLessThan( - upScript.lastIndexOf("echo \"Minikube infrastructure for '${PROFILE}' is running.\""), + upScript.lastIndexOf('echo "Minikube infrastructure for \'${PROFILE}\' is running."'), ); expect(upScript.indexOf('credentialsRef auth/oauth/google')).toBeLessThan( upScript.lastIndexOf('run_provider_reconciliation'), From 83aa3e9ca15bf39288b26ac2464bea3305e45dc2 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:38:18 +0200 Subject: [PATCH 13/18] style(auth): format redirect helpers --- src/authRedirects.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/authRedirects.ts b/src/authRedirects.ts index ae8893f..528edb9 100644 --- a/src/authRedirects.ts +++ b/src/authRedirects.ts @@ -47,10 +47,7 @@ export function resolveAuthRedirectConfiguration( export function getLocalAuthRedirectPatterns(callbackRoute: string): readonly string[] { const normalizedRoute = normalizeCallbackRoute(callbackRoute); - return [ - `http://127.0.0.1:*${normalizedRoute}`, - `http://localhost:*${normalizedRoute}`, - ]; + return [`http://127.0.0.1:*${normalizedRoute}`, `http://localhost:*${normalizedRoute}`]; } export function normalizeAuthCallbackRoute(callbackRoute: string): string { From b60cb8ec7009e2d178fc0a97a0bb3be2f6a2c9cb Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:38:32 +0200 Subject: [PATCH 14/18] style(auth): format redirect tests --- src/authRedirects.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/authRedirects.test.ts b/src/authRedirects.test.ts index 7bed74d..b449efd 100644 --- a/src/authRedirects.test.ts +++ b/src/authRedirects.test.ts @@ -1,9 +1,6 @@ import { describe, expect, test } from 'bun:test'; -import { - getLocalAuthRedirectPatterns, - resolveAuthRedirectConfiguration, -} from './authRedirects'; +import { getLocalAuthRedirectPatterns, resolveAuthRedirectConfiguration } from './authRedirects'; describe('resolveAuthRedirectConfiguration', () => { test('derives an exact provider callback and local browser callback policy', () => { @@ -16,9 +13,7 @@ describe('resolveAuthRedirectConfiguration', () => { nativeRedirectUris: ['ankh-demo://auth/callback'], }); - expect(config.providerCallbackUrl).toBe( - 'http://127.0.0.1:18081/auth/v1/callback', - ); + expect(config.providerCallbackUrl).toBe('http://127.0.0.1:18081/auth/v1/callback'); expect(config.siteUrl).toBe('http://127.0.0.1:18080'); expect(config.redirectAllowList).toEqual([ 'http://127.0.0.1:18080', From 917ec18726a78f3b515494d3fe7eae80727c24cd Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:38:43 +0200 Subject: [PATCH 15/18] style(auth): format provider assignment test --- .../minikube/auth/supabase/providerAssignments.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/adapters/minikube/auth/supabase/providerAssignments.test.ts b/src/adapters/minikube/auth/supabase/providerAssignments.test.ts index f83d81f..07fcfec 100644 --- a/src/adapters/minikube/auth/supabase/providerAssignments.test.ts +++ b/src/adapters/minikube/auth/supabase/providerAssignments.test.ts @@ -30,11 +30,7 @@ test('keeps multiple provider redirect assignments as separate shell arguments', (file) => file.path === 'infra/minikube/scripts/up.sh', )?.content; - expect(upScript).toContain( - 'GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="${oauth_provider_callback}"', - ); - expect(upScript).toContain( - 'GOTRUE_EXTERNAL_APPLE_REDIRECT_URI="${oauth_provider_callback}"', - ); + expect(upScript).toContain('GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="${oauth_provider_callback}"'); + expect(upScript).toContain('GOTRUE_EXTERNAL_APPLE_REDIRECT_URI="${oauth_provider_callback}"'); expect(upScript).not.toContain('\\ GOTRUE_EXTERNAL_APPLE_REDIRECT_URI'); }); From 0fb72c3aa73451c02aae77be94a0663cd92f5650 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:39:17 +0200 Subject: [PATCH 16/18] style(auth): sort public redirect exports --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 69279d5..c2f3125 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,12 @@ import type { export type { ApiInfrastructureArtifacts } from './apiArtifacts'; export { generateApiInfrastructureArtifacts } from './apiArtifacts'; export { - getLocalAuthRedirectPatterns, - normalizeAuthCallbackRoute, - resolveAuthRedirectConfiguration, type AuthRedirectConfiguration, type AuthRedirectEnvironment, + getLocalAuthRedirectPatterns, + normalizeAuthCallbackRoute, type ResolveAuthRedirectConfigurationInput, + resolveAuthRedirectConfiguration, } from './authRedirects'; export { createInfraSecretStoreAdapter, From 3286dce7fa1306da012bc73b0fc322a97b201219 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:40:30 +0200 Subject: [PATCH 17/18] style(auth): format OAuth runtime import --- src/adapters/minikube/auth/oauthRuntime.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/adapters/minikube/auth/oauthRuntime.ts b/src/adapters/minikube/auth/oauthRuntime.ts index 0d93de0..0284bf8 100644 --- a/src/adapters/minikube/auth/oauthRuntime.ts +++ b/src/adapters/minikube/auth/oauthRuntime.ts @@ -1,7 +1,4 @@ -import { - getLocalAuthRedirectPatterns, - normalizeAuthCallbackRoute, -} from '../../../authRedirects'; +import { getLocalAuthRedirectPatterns, normalizeAuthCallbackRoute } from '../../../authRedirects'; import type { InfraManifestInput } from '../../../types'; import type { MinikubeAdapterArtifacts, MinikubeProviderLifecycle } from '../contracts'; From ac051c9d4aa82cc67e076e5acb373c4f63e68973 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:40:44 +0200 Subject: [PATCH 18/18] style(auth): finish redirect export sorting --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index c2f3125..b6950e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,8 +13,8 @@ export { type AuthRedirectEnvironment, getLocalAuthRedirectPatterns, normalizeAuthCallbackRoute, - type ResolveAuthRedirectConfigurationInput, resolveAuthRedirectConfiguration, + type ResolveAuthRedirectConfigurationInput, } from './authRedirects'; export { createInfraSecretStoreAdapter,