@ankhorage/infra 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/dist/adapters/minikube/base/index.js +14 -6
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/minikube/auth/supabase/profile.integration.test.ts +170 -2
- package/src/adapters/minikube/base/index.test.ts +96 -0
- package/src/adapters/minikube/base/index.ts +14 -6
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# INFRA
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Executable infra provider and standalone CLI for Ankhorage project workflows.
|
|
9
9
|
|
|
@@ -948,13 +948,21 @@ $$;`;
|
|
|
948
948
|
}
|
|
949
949
|
function getSupabaseProfileDisabledGuardSql() {
|
|
950
950
|
return `do $$
|
|
951
|
+
declare
|
|
952
|
+
has_profile_state boolean := false;
|
|
951
953
|
begin
|
|
952
|
-
if to_regclass('ankhorage_internal.generated_schema_state') is not null
|
|
953
|
-
|
|
954
|
-
select
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
954
|
+
if to_regclass('ankhorage_internal.generated_schema_state') is not null then
|
|
955
|
+
execute
|
|
956
|
+
'select exists (
|
|
957
|
+
select 1
|
|
958
|
+
from ankhorage_internal.generated_schema_state
|
|
959
|
+
where artifact_key = $1
|
|
960
|
+
)'
|
|
961
|
+
into has_profile_state
|
|
962
|
+
using 'auth.profile';
|
|
963
|
+
end if;
|
|
964
|
+
|
|
965
|
+
if has_profile_state then
|
|
958
966
|
raise exception 'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration';
|
|
959
967
|
end if;
|
|
960
968
|
end;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/adapters/minikube/base/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { GeneratedInfrastructureFile } from '../../../types';\nimport type { InfraManifestInput } from '../../../types';\nimport {\n MANAGED_PROFILE_COLUMNS,\n type ResolvedProfileModel,\n resolveSupabaseProfileModel,\n} from '../auth/supabase/profile';\n\ninterface SupabaseLocalPorts {\n base: number;\n shadow: number;\n api: number;\n db: number;\n studio: number;\n inbucket: number;\n analytics: number;\n}\n\ninterface AppInfraStorageMetadata {\n provider: 'supabase';\n bucketsCsv: string;\n defaultBucket: string;\n}\n\nconst SUPABASE_LOCAL_PORT_BASE = 55320;\nconst SUPABASE_LOCAL_PORT_BUCKET_SIZE = 10;\nconst SUPABASE_LOCAL_PORT_BUCKET_COUNT = 1000;\nconst SUPABASE_LOCAL_PORT_REFERENCE_PROJECT = 'my-app';\n\nexport function generateMinikubeBaseArtifacts(args: {\n manifest: InfraManifestInput;\n namespace: string;\n supabaseProjectId: string | null;\n extraResources: string[];\n extraEnvEntries: string[];\n}): GeneratedInfrastructureFile[] {\n const { manifest, namespace, supabaseProjectId, extraResources, extraEnvEntries } = args;\n\n const root = 'infra/minikube';\n const k8sRoot = `${root}/k8s`;\n const scriptsRoot = `${root}/scripts`;\n const appImageRoot = `${root}/app-image`;\n const defaultAppImage = getDefaultAppImage(namespace);\n const supabaseLocalPorts = resolveSupabaseLocalPorts(supabaseProjectId ?? namespace);\n\n const authScope = manifest.auth?.scope ?? 'none';\n const authProvider = manifest.auth?.provider ?? 'none';\n const authzEngine = manifest.auth?.authorization?.engine ?? 'none';\n const databaseProvider = manifest.database?.provider ?? 'none';\n const secretStoreProvider = manifest.secretStore?.provider ?? 'none';\n const storageMetadata = resolveAppInfraStorageMetadata(manifest);\n const profileModel = resolveSupabaseProfileModel(manifest);\n const monitoringEnabled = manifest.deployment?.monitoring === true;\n const domain = manifest.networking?.domain ?? '';\n const supabaseLocalEnabled =\n authProvider === 'supabase' ||\n databaseProvider === 'supabase' ||\n storageMetadata !== null ||\n secretStoreProvider === 'supabase-vault';\n\n return [\n {\n path: `${root}/README.md`,\n content: getReadmeMarkdown({\n namespace,\n defaultAppImage,\n monitoringEnabled,\n authProvider,\n authzEngine,\n extraResources,\n supabaseLocalPorts,\n supabaseProjectId,\n supabaseLocalEnabled,\n profileModel,\n }),\n },\n {\n path: `${root}/.env.example`,\n content: getEnvExample({\n namespace,\n domain,\n extraEnvEntries,\n defaultAppImage,\n supabaseLocalPorts,\n supabaseLocalEnabled,\n }),\n },\n {\n path: `${k8sRoot}/namespace.yaml`,\n content: getNamespaceManifest(namespace),\n },\n {\n path: `${k8sRoot}/app.configmap.yaml`,\n content: getAppConfigMap({\n namespace,\n authScope,\n authProvider,\n databaseProvider,\n secretStoreProvider,\n storageMetadata,\n monitoringEnabled,\n domain,\n }),\n },\n {\n path: `${k8sRoot}/app/deployment.yaml`,\n content: getAppDeploymentManifest({ namespace, defaultAppImage }),\n },\n {\n path: `${k8sRoot}/app/service.yaml`,\n content: getAppServiceManifest(namespace),\n },\n {\n path: `${k8sRoot}/kustomization.yaml`,\n content: getKustomizationManifest(extraResources),\n },\n {\n path: `${scriptsRoot}/up.sh`,\n content: getUpScript({\n defaultNamespace: namespace,\n defaultAppImage,\n supabaseLocalEnabled,\n }),\n executable: true,\n },\n {\n path: `${scriptsRoot}/build-app-image.sh`,\n content: getBuildAppImageScript({ defaultNamespace: namespace, defaultAppImage }),\n executable: true,\n },\n {\n path: `${scriptsRoot}/port-forward.sh`,\n content: getPortForwardScript(namespace),\n executable: true,\n },\n {\n path: `${scriptsRoot}/down.sh`,\n content: getDownScript(),\n executable: true,\n },\n {\n path: `${scriptsRoot}/status.sh`,\n content: getStatusScript({\n defaultNamespace: namespace,\n supabaseProjectId,\n supabaseLocalEnabled,\n profileModel,\n }),\n executable: true,\n },\n {\n path: `${scriptsRoot}/supabase-local-env.sh`,\n content: getSupabaseLocalEnvScript({\n supabaseProjectId,\n supabaseLocalPorts,\n profileModel,\n }),\n executable: true,\n },\n {\n path: `${appImageRoot}/Dockerfile`,\n content: getAppImageDockerfile(),\n },\n ];\n}\n\nfunction getReadmeMarkdown(args: {\n namespace: string;\n defaultAppImage: string;\n monitoringEnabled: boolean;\n authProvider: string;\n authzEngine: string;\n extraResources: string[];\n supabaseLocalPorts: SupabaseLocalPorts;\n supabaseProjectId: string | null;\n supabaseLocalEnabled: boolean;\n profileModel: ResolvedProfileModel;\n}): string {\n const {\n namespace,\n defaultAppImage,\n monitoringEnabled,\n authProvider,\n authzEngine,\n extraResources,\n supabaseLocalPorts,\n supabaseProjectId,\n supabaseLocalEnabled,\n profileModel,\n } = args;\n const resourceLines = [\n 'namespace.yaml',\n 'app.configmap.yaml',\n 'app/deployment.yaml',\n 'app/service.yaml',\n ...extraResources,\n ]\n .map((r) => `- \\`k8s/${r}\\``)\n .join('\\n');\n\n return `# Minikube Infra\n\nThis directory is generated from \\`ankh.config.json\\` (infra manifest).\n\n## Requirements\n\n- minikube\n- kubectl\n- docker (when using the default driver)\n- bun (for Expo web export in app image build flow)\n- python3 (for patching generated local Supabase port defaults and checking port availability)\n\n## Quick Start\n\n1. Copy env template:\n \\`\\`\\`bash\n cp .env.example .env\n \\`\\`\\`\n2. Optional: import local Supabase credentials from \\`supabase status\\` output:\n \\`\\`\\`bash\n ./scripts/supabase-local-env.sh\n \\`\\`\\`\n3. Apply baseline infra:\n \\`\\`\\`bash\n ./scripts/up.sh\n \\`\\`\\`\n4. Start app port-forward:\n \\`\\`\\`bash\n ./scripts/port-forward.sh\n \\`\\`\\`\n5. Check status:\n \\`\\`\\`bash\n ./scripts/status.sh\n \\`\\`\\`\n6. Tear down:\n \\`\\`\\`bash\n ./scripts/down.sh\n \\`\\`\\`\n\n## Generated Resources\n\n${resourceLines}\n\n## Generated Helpers\n\n- \\`scripts/build-app-image.sh\\`\n- \\`scripts/port-forward.sh\\`\n- \\`scripts/supabase-local-env.sh\\`\n- \\`app-image/Dockerfile\\`\n\n## Runtime Conventions\n\n- Namespace: \\`${namespace}\\`\n- Supabase local project identity: \\`${supabaseProjectId ?? 'unused'}\\`\n- Monitoring requested: \\`${monitoringEnabled ? 'true' : 'false'}\\`\n- Auth provider: \\`${authProvider}\\`\n- Authorization engine: \\`${authzEngine}\\`\n- Namespace source: \\`infra.networking.domain\\` first, otherwise project slug hint from CLI.\n\n## App Runtime\n\nDefault app runtime manifests are generated:\n\n- \\`k8s/app/deployment.yaml\\`\n- \\`k8s/app/service.yaml\\`\n\nDeployment env wiring includes:\n\n- \\`ConfigMap/app-infra-config\\`\n- \\`ConfigMap/app-runtime-auth-env\\` (optional)\n- \\`ConfigMap/app-runtime-storage-env\\` (optional)\n- \\`Secret/supabase-auth-secrets\\` (optional)\n\nIf \\`auth.provider\\` is \\`supabase\\`, use:\n\n- \\`k8s/auth/supabase/supabase-auth.secret.yaml\\`\n- \\`k8s/auth/supabase/app-runtime-auth.env.configmap.yaml\\`\n- \\`auth/supabase-runtime-wiring.md\\`\n\nThe app deployment should import runtime config sources in this order:\n\n\\`\\`\\`yaml\nenvFrom:\n - configMapRef:\n name: app-runtime-auth-env\n - configMapRef:\n name: app-runtime-storage-env\n - secretRef:\n name: supabase-auth-secrets\n\\`\\`\\`\n\nDefault runtime knobs in \\`.env\\`:\n\n- \\`APP_BUILD_ENABLED\\`\n- \\`APP_SOURCE_DIR\\`\n- \\`APP_WEB_EXPORT_DIR\\`\n- \\`APP_IMAGE\\`\n- \\`APP_IMAGE_SYNC_STRATEGY\\`\n- \\`APP_IMAGE_CLEANUP_ON_DOWN\\`\n- \\`APP_IMAGE_CLEANUP_MINIKUBE\\`\n- \\`APP_IMAGE_CLEANUP_DOCKER\\`\n- \\`AUTH_RUNTIME_MODE\\`\n- \\`APP_PORT_FORWARD_LOCAL_PORT\\`\n- \\`APP_PORT_FORWARD_REMOTE_PORT\\`\n- \\`APP_REPLICAS\\`\n- \\`APP_FORCE_ROLLOUT_RESTART\\`\n- \\`SUPABASE_PROJECT_DIR\\` (optional supabase CLI project path)\n- \\`SUPABASE_LOCAL_PORT_BASE\\` (generated default \\`${supabaseLocalPorts.base}\\`)\n- \\`SUPABASE_LOCAL_API_PORT\\` (generated default \\`${supabaseLocalPorts.api}\\`)\n- \\`SUPABASE_LOCAL_DB_PORT\\` (generated default \\`${supabaseLocalPorts.db}\\`)\n- \\`SUPABASE_LOCAL_STUDIO_PORT\\` (generated default \\`${supabaseLocalPorts.studio}\\`)\n- \\`SUPABASE_LOCAL_INBUCKET_PORT\\` (generated default \\`${supabaseLocalPorts.inbucket}\\`)\n- \\`SUPABASE_LOCAL_ANALYTICS_PORT\\` (generated default \\`${supabaseLocalPorts.analytics}\\`)\n- \\`SUPABASE_LOCAL_SHADOW_PORT\\` (generated default \\`${supabaseLocalPorts.shadow}\\`)\n- \\`APP_IMAGE_PULL_SECRET_NAME\\` + credentials (optional, private registry)\n- \\`SUPABASE_SECRET_SYNC_ENABLED\\`\n\nGenerated app Supabase local defaults:\n\n- Port base: \\`${supabaseLocalPorts.base}\\`\n- API: \\`http://127.0.0.1:${supabaseLocalPorts.api}\\`\n- DB: \\`127.0.0.1:${supabaseLocalPorts.db}\\`\n- Studio: \\`http://127.0.0.1:${supabaseLocalPorts.studio}\\`\n- Inbucket: \\`http://127.0.0.1:${supabaseLocalPorts.inbucket}\\`\n- Analytics: \\`http://127.0.0.1:${supabaseLocalPorts.analytics}\\`\n- Shadow DB: \\`127.0.0.1:${supabaseLocalPorts.shadow}\\`\n\nOverride with:\n\n- \\`SUPABASE_LOCAL_PORT_BASE\\`\n- \\`SUPABASE_LOCAL_API_PORT\\`\n- \\`SUPABASE_LOCAL_DB_PORT\\`\n- \\`SUPABASE_LOCAL_STUDIO_PORT\\`\n- \\`SUPABASE_LOCAL_INBUCKET_PORT\\`\n- \\`SUPABASE_LOCAL_ANALYTICS_PORT\\`\n- \\`SUPABASE_LOCAL_SHADOW_PORT\\`\n\nBuild flow:\n\n- \\`./scripts/up.sh\\` calls \\`./scripts/build-app-image.sh\\` when \\`APP_BUILD_ENABLED=true\\`.\n- Supabase local project root: \\`infra/minikube\\`${supabaseLocalEnabled ? '' : ' (unused unless Supabase-backed auth/database/storage is enabled)'}.\n- Supabase local project identity: \\`${supabaseProjectId ?? 'unused'}\\`; existing local configs with another \\`project_id\\` must be destroyed and recreated manually.\n- Immutable migrations live in \\`supabase/migrations/\\` and are applied with \\`supabase migration up --local\\`.\n${profileModel.enabled ? '- Generated profile desired-state reconciliation lives in `supabase/generated/auth_profiles.sql` and records checksum state in `ankhorage_internal.generated_schema_state`.\\n' : ''}- \\`./scripts/supabase-local-env.sh\\` can populate Supabase keys in \\`.env\\` from local \\`supabase status -o env\\`.\n- \\`./scripts/supabase-local-env.sh\\` writes \\`EXPO_PUBLIC_SUPABASE_URL\\` + \\`EXPO_PUBLIC_SUPABASE_ANON_KEY\\` into \\`$APP_SOURCE_DIR/.env.local\\` (e.g. \\`apps/card/.env.local\\`) for local app runs.\n- With \\`AUTH_RUNTIME_MODE=local\\` (default), \\`up.sh\\` runs \\`supabase-local-env.sh\\` before app/Kubernetes startup when Supabase-backed local services are enabled.\n- \\`supabase-local-env.sh\\` checks configured host ports before starting a missing local Supabase stack.\n- \\`build-app-image.sh\\` runs \\`bunx expo export --platform web\\` from app source.\n- \\`app-image/Dockerfile\\` builds an nginx image from exported web artifacts.\n- \\`build-app-image.sh\\` labels the generated Docker image with Ankhorage metadata for diagnostics.\n- \\`up.sh\\` syncs runtime image by strategy:\n - \\`APP_IMAGE_SYNC_STRATEGY=docker-load\\` (default): load prebuilt local Docker image into Minikube.\n - \\`APP_IMAGE_SYNC_STRATEGY=minikube-build\\`: build directly into Minikube image store.\n - \\`APP_IMAGE_SYNC_STRATEGY=none\\`: skip local image sync.\n- \\`up.sh\\` applies image/replica changes and (by default) restarts \\`deployment/app-runtime\\`.\n- If Supabase keys are provided, \\`up.sh\\` syncs \\`Secret/supabase-auth-secrets\\` from env.\n\nDefault generated app image tag: \\`${defaultAppImage}\\`.\n\n## Notes\n\n- Auth/database/authorization resources are generated in adapter modules.\n- Cerbos route/screen policies derive from app navigator + authFlow intent when available.\n- Re-generate infra by saving the manifest or calling the bridge infra endpoint.\n`;\n}\n\nfunction getEnvExample(args: {\n namespace: string;\n domain: string;\n extraEnvEntries: string[];\n defaultAppImage: string;\n supabaseLocalPorts: SupabaseLocalPorts;\n supabaseLocalEnabled: boolean;\n}): string {\n const { namespace, domain, extraEnvEntries, defaultAppImage, supabaseLocalPorts } = args;\n\n const baseEntries = [\n '# Minikube runtime configuration',\n `ANKH_NAMESPACE=${namespace}`,\n 'MINIKUBE_PROFILE=minikube',\n 'MINIKUBE_DRIVER=docker',\n 'APP_BUILD_ENABLED=true',\n 'APP_SOURCE_DIR=',\n 'APP_WEB_EXPORT_DIR=.ankh/web-export',\n '# Canonical Supabase project root. Leave empty to use infra/minikube.',\n 'SUPABASE_PROJECT_DIR=',\n `SUPABASE_LOCAL_PORT_BASE=${supabaseLocalPorts.base}`,\n `SUPABASE_LOCAL_SHADOW_PORT=${supabaseLocalPorts.shadow}`,\n `SUPABASE_LOCAL_API_PORT=${supabaseLocalPorts.api}`,\n `SUPABASE_LOCAL_DB_PORT=${supabaseLocalPorts.db}`,\n `SUPABASE_LOCAL_STUDIO_PORT=${supabaseLocalPorts.studio}`,\n `SUPABASE_LOCAL_INBUCKET_PORT=${supabaseLocalPorts.inbucket}`,\n `SUPABASE_LOCAL_ANALYTICS_PORT=${supabaseLocalPorts.analytics}`,\n `APP_IMAGE=${defaultAppImage}`,\n 'APP_IMAGE_SYNC_STRATEGY=docker-load',\n 'APP_IMAGE_CLEANUP_ON_DOWN=true',\n 'APP_IMAGE_CLEANUP_MINIKUBE=true',\n 'APP_IMAGE_CLEANUP_DOCKER=true',\n 'AUTH_RUNTIME_MODE=local',\n 'APP_PORT_FORWARD_LOCAL_PORT=18080',\n 'APP_PORT_FORWARD_REMOTE_PORT=80',\n 'APP_REPLICAS=1',\n 'APP_FORCE_ROLLOUT_RESTART=true',\n '',\n '# Optional private registry auth for APP_IMAGE',\n 'APP_IMAGE_PULL_SECRET_NAME=',\n 'APP_IMAGE_PULL_SECRET_SERVER=ghcr.io',\n 'APP_IMAGE_PULL_SECRET_USERNAME=',\n 'APP_IMAGE_PULL_SECRET_PASSWORD=',\n 'APP_IMAGE_PULL_SECRET_EMAIL=',\n '',\n '# Optional app networking domain',\n `APP_DOMAIN=${domain}`,\n '',\n '# Adapter-provided runtime keys',\n ];\n\n const merged = [...baseEntries, ...extraEnvEntries];\n const unique: string[] = [];\n for (const entry of merged) {\n if (!unique.includes(entry)) unique.push(entry);\n }\n\n return `${unique.join('\\n')}\\n`;\n}\n\nfunction getNamespaceManifest(namespace: string): string {\n return `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${namespace}\n labels:\n app.kubernetes.io/part-of: ankhorage\n`;\n}\n\nfunction getAppConfigMap(args: {\n namespace: string;\n authScope: string;\n authProvider: string;\n databaseProvider: string;\n secretStoreProvider: string;\n storageMetadata: AppInfraStorageMetadata | null;\n monitoringEnabled: boolean;\n domain: string;\n}): string {\n const {\n namespace,\n authScope,\n authProvider,\n databaseProvider,\n secretStoreProvider,\n storageMetadata,\n monitoringEnabled,\n domain,\n } = args;\n\n const storageLines = storageMetadata\n ? ` STORAGE_PROVIDER: \"${storageMetadata.provider}\"\n STORAGE_BUCKETS: \"${escapeYamlDoubleQuoted(storageMetadata.bucketsCsv)}\"\n STORAGE_DEFAULT_BUCKET: \"${escapeYamlDoubleQuoted(storageMetadata.defaultBucket)}\"\n`\n : '';\n\n return `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app-infra-config\n namespace: ${namespace}\ndata:\n DEPLOYMENT_TARGET: \"minikube\"\n MONITORING_ENABLED: \"${monitoringEnabled ? 'true' : 'false'}\"\n AUTH_SCOPE: \"${authScope}\"\n AUTH_PROVIDER: \"${authProvider}\"\n DATABASE_PROVIDER: \"${databaseProvider}\"\n SECRET_STORE_PROVIDER: \"${secretStoreProvider}\"\n${storageLines} NETWORK_DOMAIN: \"${domain}\"\n`;\n}\n\nfunction resolveAppInfraStorageMetadata(\n manifest: InfraManifestInput,\n): AppInfraStorageMetadata | null {\n const spec = manifest.storage;\n if (!spec) return null;\n\n const buckets = normalizeBuckets(spec.buckets);\n if (buckets.length === 0) return null;\n const [defaultBucket] = buckets;\n if (!defaultBucket) return null;\n\n if (spec.provider === 'supabase') {\n return {\n provider: 'supabase',\n bucketsCsv: buckets.join(','),\n defaultBucket,\n };\n }\n\n if (spec.provider === 'auto') {\n const shouldResolveToSupabase =\n manifest.database?.provider === 'supabase' || manifest.auth?.provider === 'supabase';\n\n if (!shouldResolveToSupabase) return null;\n\n return {\n provider: 'supabase',\n bucketsCsv: buckets.join(','),\n defaultBucket,\n };\n }\n\n return null;\n}\n\nfunction normalizeBuckets(rawBuckets: readonly string[]): string[] {\n const normalized: string[] = [];\n const seen = new Set<string>();\n\n for (const raw of rawBuckets) {\n const bucket = raw.trim();\n if (!bucket) continue;\n if (seen.has(bucket)) continue;\n seen.add(bucket);\n normalized.push(bucket);\n }\n\n return normalized;\n}\n\nfunction escapeYamlDoubleQuoted(value: string): string {\n return value.replaceAll('\\\\', '\\\\\\\\').replaceAll('\"', '\\\\\"');\n}\n\nfunction getKustomizationManifest(extraResources: string[]): string {\n const resources = [\n 'namespace.yaml',\n 'app.configmap.yaml',\n 'app/deployment.yaml',\n 'app/service.yaml',\n ...extraResources,\n ];\n const resourceLines = resources.map((r) => ` - ${r}`).join('\\n');\n\n return `apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n${resourceLines}\n`;\n}\n\nfunction getAppDeploymentManifest(args: { namespace: string; defaultAppImage: string }): string {\n const { namespace, defaultAppImage } = args;\n\n return `apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app-runtime\n namespace: ${namespace}\n labels:\n app.kubernetes.io/name: app-runtime\nspec:\n replicas: 1\n selector:\n matchLabels:\n app.kubernetes.io/name: app-runtime\n template:\n metadata:\n labels:\n app.kubernetes.io/name: app-runtime\n spec:\n containers:\n - name: app\n image: ${defaultAppImage}\n imagePullPolicy: IfNotPresent\n ports:\n - containerPort: 8080\n name: http\n envFrom:\n - configMapRef:\n name: app-infra-config\n - configMapRef:\n name: app-runtime-auth-env\n optional: true\n - configMapRef:\n name: app-runtime-storage-env\n optional: true\n - secretRef:\n name: supabase-auth-secrets\n optional: true\n readinessProbe:\n httpGet:\n path: /\n port: http\n initialDelaySeconds: 5\n periodSeconds: 10\n livenessProbe:\n httpGet:\n path: /\n port: http\n initialDelaySeconds: 15\n periodSeconds: 20\n`;\n}\n\nfunction getAppServiceManifest(namespace: string): string {\n return `apiVersion: v1\nkind: Service\nmetadata:\n name: app-runtime\n namespace: ${namespace}\n labels:\n app.kubernetes.io/name: app-runtime\nspec:\n type: ClusterIP\n selector:\n app.kubernetes.io/name: app-runtime\n ports:\n - name: http\n port: 80\n targetPort: http\n`;\n}\n\nfunction getUpScript(args: {\n defaultNamespace: string;\n defaultAppImage: string;\n supabaseLocalEnabled: boolean;\n}): string {\n const { defaultNamespace, defaultAppImage, supabaseLocalEnabled } = args;\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\nK8S_DIR=\"\\${ROOT_DIR}/k8s\"\nBUILD_SCRIPT=\"\\${SCRIPT_DIR}/build-app-image.sh\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nPROFILE=\"\\${MINIKUBE_PROFILE:-minikube}\"\nDRIVER=\"\\${MINIKUBE_DRIVER:-docker}\"\nNAMESPACE=\"\\${ANKH_NAMESPACE:-${defaultNamespace}}\"\nAPP_BUILD_ENABLED=\"\\${APP_BUILD_ENABLED:-true}\"\nAPP_SOURCE_DIR=\"\\${APP_SOURCE_DIR:-$(cd \"\\${ROOT_DIR}/../..\" && pwd)}\"\nAPP_WEB_EXPORT_DIR=\"\\${APP_WEB_EXPORT_DIR:-.ankh/web-export}\"\nAPP_IMAGE=\"\\${APP_IMAGE:-${defaultAppImage}}\"\nAPP_IMAGE_SYNC_STRATEGY=\"\\${APP_IMAGE_SYNC_STRATEGY:-docker-load}\"\nAPP_REPLICAS=\"\\${APP_REPLICAS:-1}\"\nAPP_FORCE_ROLLOUT_RESTART=\"\\${APP_FORCE_ROLLOUT_RESTART:-true}\"\nAPP_IMAGE_PULL_SECRET_NAME=\"\\${APP_IMAGE_PULL_SECRET_NAME:-}\"\nAPP_IMAGE_PULL_SECRET_SERVER=\"\\${APP_IMAGE_PULL_SECRET_SERVER:-ghcr.io}\"\nAPP_IMAGE_PULL_SECRET_USERNAME=\"\\${APP_IMAGE_PULL_SECRET_USERNAME:-}\"\nAPP_IMAGE_PULL_SECRET_PASSWORD=\"\\${APP_IMAGE_PULL_SECRET_PASSWORD:-}\"\nAPP_IMAGE_PULL_SECRET_EMAIL=\"\\${APP_IMAGE_PULL_SECRET_EMAIL:-}\"\nAUTH_RUNTIME_MODE=\"\\${AUTH_RUNTIME_MODE:-local}\"\nSUPABASE_LOCAL_ENABLED=\"\\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}\"\nSUPABASE_SECRET_SYNC_ENABLED=\"\\${SUPABASE_SECRET_SYNC_ENABLED:-true}\"\nSUPABASE_LOCAL_ENV_SCRIPT=\"\\${SCRIPT_DIR}/supabase-local-env.sh\"\nSUPABASE_URL=\"\\${SUPABASE_URL:-}\"\nSUPABASE_ANON_KEY=\"\\${SUPABASE_ANON_KEY:-}\"\nSUPABASE_SERVICE_ROLE_KEY=\"\\${SUPABASE_SERVICE_ROLE_KEY:-}\"\nSUPABASE_JWT_SECRET=\"\\${SUPABASE_JWT_SECRET:-}\"\nSUPABASE_PUBLIC_URL=\"\\${EXPO_PUBLIC_SUPABASE_URL:-\\${SUPABASE_URL}}\"\nSUPABASE_PUBLIC_ANON_KEY=\"\\${EXPO_PUBLIC_SUPABASE_ANON_KEY:-\\${SUPABASE_ANON_KEY}}\"\n\nrefresh_supabase_env() {\n SUPABASE_URL=\"\\${SUPABASE_URL:-}\"\n SUPABASE_ANON_KEY=\"\\${SUPABASE_ANON_KEY:-}\"\n SUPABASE_SERVICE_ROLE_KEY=\"\\${SUPABASE_SERVICE_ROLE_KEY:-}\"\n SUPABASE_JWT_SECRET=\"\\${SUPABASE_JWT_SECRET:-}\"\n SUPABASE_PUBLIC_URL=\"\\${EXPO_PUBLIC_SUPABASE_URL:-\\${SUPABASE_URL}}\"\n SUPABASE_PUBLIC_ANON_KEY=\"\\${EXPO_PUBLIC_SUPABASE_ANON_KEY:-\\${SUPABASE_ANON_KEY}}\"\n}\n\nhas_required_supabase_env() {\n [[ -n \"\\${SUPABASE_URL}\" && -n \"\\${SUPABASE_ANON_KEY}\" && -n \"\\${SUPABASE_SERVICE_ROLE_KEY}\" && -n \"\\${SUPABASE_JWT_SECRET}\" ]]\n}\n\nrefresh_supabase_env\n\nif [[ \"\\${AUTH_RUNTIME_MODE}\" != \"local\" && \"\\${AUTH_RUNTIME_MODE}\" != \"remote\" ]]; then\n echo \"Unsupported AUTH_RUNTIME_MODE=\\${AUTH_RUNTIME_MODE}. Use local or remote.\"\n exit 1\nfi\n\nif ! command -v minikube >/dev/null 2>&1; then\n echo \"minikube is required but not installed.\"\n exit 1\nfi\n\nif ! command -v kubectl >/dev/null 2>&1; then\n echo \"kubectl is required but not installed.\"\n exit 1\nfi\n\nHOST_STATUS=\"$(minikube -p \"\\${PROFILE}\" status --format='{{.Host}}' 2>/dev/null || true)\"\nif [[ \"\\${HOST_STATUS}\" != \"Running\" ]]; then\n minikube start -p \"\\${PROFILE}\" --driver=\"\\${DRIVER}\"\nfi\n\nif [[ \"\\${AUTH_RUNTIME_MODE}\" == \"local\" && \"\\${SUPABASE_LOCAL_ENABLED}\" == \"true\" ]]; then\n if [[ ! -x \"\\${SUPABASE_LOCAL_ENV_SCRIPT}\" ]]; then\n echo \"Supabase local bootstrap failed: missing executable \\${SUPABASE_LOCAL_ENV_SCRIPT}.\"\n exit 1\n else\n echo \"Running local Supabase bootstrap, immutable migrations, generated reconciliation, and schema verification.\"\n if ! \"\\${SUPABASE_LOCAL_ENV_SCRIPT}\"; then\n echo \"Supabase local bootstrap failed.\"\n exit 1\n fi\n if [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\n fi\n refresh_supabase_env\n fi\nfi\n\nif [[ \"\\${APP_BUILD_ENABLED}\" == \"true\" ]]; then\n if [[ ! -x \"\\${BUILD_SCRIPT}\" ]]; then\n echo \"Missing build helper script: \\${BUILD_SCRIPT}\"\n exit 1\n fi\n\n \"\\${BUILD_SCRIPT}\"\nfi\n\nif [[ \"\\${APP_WEB_EXPORT_DIR}\" = /* ]]; then\n EXPORT_DIR=\"\\${APP_WEB_EXPORT_DIR}\"\nelse\n EXPORT_DIR=\"\\${APP_SOURCE_DIR}/\\${APP_WEB_EXPORT_DIR}\"\nfi\n\ndocker_load_image_to_minikube() {\n if command -v docker >/dev/null 2>&1 && docker image inspect \"\\${APP_IMAGE}\" >/dev/null 2>&1; then\n minikube -p \"\\${PROFILE}\" image load --daemon=true --overwrite=true \"\\${APP_IMAGE}\" >/dev/null\n return 0\n fi\n\n return 1\n}\n\ncase \"\\${APP_IMAGE_SYNC_STRATEGY}\" in\n minikube-build)\n if [[ ! -f \"\\${EXPORT_DIR}/index.html\" ]]; then\n echo \"Expected web export output not found at \\${EXPORT_DIR}/index.html\"\n echo \"Run build-app-image.sh first or switch APP_IMAGE_SYNC_STRATEGY=docker-load.\"\n exit 1\n fi\n\n if minikube -p \"\\${PROFILE}\" image build -t \"\\${APP_IMAGE}\" -f \"\\${ROOT_DIR}/app-image/Dockerfile\" \"\\${EXPORT_DIR}\" >/dev/null; then\n if ! minikube -p \"\\${PROFILE}\" image ls 2>/dev/null | grep -Fq \"\\${APP_IMAGE}\"; then\n echo \"minikube image build did not register \\${APP_IMAGE}; falling back to docker-load.\"\n if ! docker_load_image_to_minikube; then\n echo \"Fallback docker-load failed: local Docker image \\${APP_IMAGE} not found.\"\n exit 1\n fi\n fi\n else\n echo \"minikube image build failed for \\${APP_IMAGE}; falling back to docker-load.\"\n if ! docker_load_image_to_minikube; then\n echo \"Fallback docker-load failed: local Docker image \\${APP_IMAGE} not found.\"\n exit 1\n fi\n fi\n ;;\n docker-load)\n if ! docker_load_image_to_minikube; then\n echo \"Skipping docker-load image sync; local Docker image \\${APP_IMAGE} not found.\"\n fi\n ;;\n none)\n echo \"Skipping local image sync (APP_IMAGE_SYNC_STRATEGY=none).\"\n ;;\n *)\n echo \"Unsupported APP_IMAGE_SYNC_STRATEGY=\\${APP_IMAGE_SYNC_STRATEGY}. Use minikube-build, docker-load, or none.\"\n exit 1\n ;;\nesac\n\nkubectl apply -k \"\\${K8S_DIR}\"\n\nif [[ \"\\${SUPABASE_SECRET_SYNC_ENABLED}\" == \"true\" ]]; then\n if ! has_required_supabase_env && [[ \"\\${AUTH_RUNTIME_MODE}\" == \"local\" && \"\\${SUPABASE_LOCAL_ENABLED}\" == \"true\" ]]; then\n if [[ ! -x \"\\${SUPABASE_LOCAL_ENV_SCRIPT}\" ]]; then\n echo \"Supabase local bootstrap failed: missing executable \\${SUPABASE_LOCAL_ENV_SCRIPT}.\"\n exit 1\n else\n echo \"Supabase keys missing; running \\${SUPABASE_LOCAL_ENV_SCRIPT}.\"\n if ! \"\\${SUPABASE_LOCAL_ENV_SCRIPT}\"; then\n echo \"Supabase local bootstrap failed.\"\n exit 1\n fi\n if [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\n fi\n refresh_supabase_env\n fi\n fi\n\n if ! has_required_supabase_env; then\n if [[ \"\\${AUTH_RUNTIME_MODE}\" == \"local\" ]]; then\n echo \"Supabase secret sync skipped: local credentials still missing after bootstrap.\"\n echo \"Run ./scripts/supabase-local-env.sh and retry.\"\n else\n echo \"Supabase secret sync skipped: set SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, and SUPABASE_JWT_SECRET in .env.\"\n fi\n else\n TMP_SUPABASE_ENV_FILE=\"$(mktemp)\"\n trap 'rm -f \"\\${TMP_SUPABASE_ENV_FILE}\"' EXIT\n\n cat > \"\\${TMP_SUPABASE_ENV_FILE}\" <<EOF\nSUPABASE_URL=\\${SUPABASE_URL}\nSUPABASE_ANON_KEY=\\${SUPABASE_ANON_KEY}\nSUPABASE_SERVICE_ROLE_KEY=\\${SUPABASE_SERVICE_ROLE_KEY}\nSUPABASE_JWT_SECRET=\\${SUPABASE_JWT_SECRET}\nEXPO_PUBLIC_SUPABASE_URL=\\${SUPABASE_PUBLIC_URL}\nEXPO_PUBLIC_SUPABASE_ANON_KEY=\\${SUPABASE_PUBLIC_ANON_KEY}\nEOF\n\n kubectl -n \"\\${NAMESPACE}\" create secret generic supabase-auth-secrets --from-env-file=\"\\${TMP_SUPABASE_ENV_FILE}\" --dry-run=client -o yaml | kubectl -n \"\\${NAMESPACE}\" apply -f -\n\n rm -f \"\\${TMP_SUPABASE_ENV_FILE}\"\n trap - EXIT\n echo \"Synchronized supabase-auth-secrets from environment values.\"\n fi\nfi\n\nif [[ -n \"\\${APP_IMAGE_PULL_SECRET_NAME}\" ]]; then\n if [[ -z \"\\${APP_IMAGE_PULL_SECRET_USERNAME}\" || -z \"\\${APP_IMAGE_PULL_SECRET_PASSWORD}\" ]]; then\n echo \"APP_IMAGE_PULL_SECRET_NAME is set, but username/password are missing.\"\n exit 1\n fi\n\n SECRET_EMAIL_ARG=()\n if [[ -n \"\\${APP_IMAGE_PULL_SECRET_EMAIL}\" ]]; then\n SECRET_EMAIL_ARG=(--docker-email=\"\\${APP_IMAGE_PULL_SECRET_EMAIL}\")\n fi\n\n kubectl -n \"\\${NAMESPACE}\" create secret docker-registry \"\\${APP_IMAGE_PULL_SECRET_NAME}\" --docker-server=\"\\${APP_IMAGE_PULL_SECRET_SERVER}\" --docker-username=\"\\${APP_IMAGE_PULL_SECRET_USERNAME}\" --docker-password=\"\\${APP_IMAGE_PULL_SECRET_PASSWORD}\" \"\\${SECRET_EMAIL_ARG[@]}\" --dry-run=client -o yaml | kubectl -n \"\\${NAMESPACE}\" apply -f -\n\n PULL_SECRET_PATCH=\"$(cat <<EOF\n{\"spec\":{\"template\":{\"spec\":{\"imagePullSecrets\":[{\"name\":\"\\${APP_IMAGE_PULL_SECRET_NAME}\"}]}}}}\nEOF\n)\"\n kubectl -n \"\\${NAMESPACE}\" patch deployment app-runtime --type=merge --patch \"\\${PULL_SECRET_PATCH}\" >/dev/null\nelse\n kubectl -n \"\\${NAMESPACE}\" patch deployment app-runtime --type=json --patch='[{\"op\":\"remove\",\"path\":\"/spec/template/spec/imagePullSecrets\"}]' >/dev/null 2>&1 || true\nfi\n\nkubectl -n \"\\${NAMESPACE}\" set image deployment/app-runtime app=\"\\${APP_IMAGE}\" >/dev/null\nkubectl -n \"\\${NAMESPACE}\" scale deployment/app-runtime --replicas=\"\\${APP_REPLICAS}\" >/dev/null\n\nif [[ \"\\${APP_FORCE_ROLLOUT_RESTART}\" == \"true\" ]]; then\n kubectl -n \"\\${NAMESPACE}\" rollout restart deployment/app-runtime >/dev/null\nfi\n\nkubectl -n \"\\${NAMESPACE}\" rollout status deployment/app-runtime --timeout=180s >/dev/null\necho \"Minikube infrastructure applied.\"\n`;\n}\n\nfunction getBuildAppImageScript(args: {\n defaultNamespace: string;\n defaultAppImage: string;\n}): string {\n const { defaultNamespace, defaultAppImage } = args;\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\nDOCKERFILE_PATH=\"\\${ROOT_DIR}/app-image/Dockerfile\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nNAMESPACE=\"\\${ANKH_NAMESPACE:-${defaultNamespace}}\"\nAPP_BUILD_ENABLED=\"\\${APP_BUILD_ENABLED:-true}\"\nAPP_SOURCE_DIR=\"\\${APP_SOURCE_DIR:-$(cd \"\\${ROOT_DIR}/../..\" && pwd)}\"\nAPP_WEB_EXPORT_DIR=\"\\${APP_WEB_EXPORT_DIR:-.ankh/web-export}\"\nAPP_IMAGE=\"\\${APP_IMAGE:-${defaultAppImage}}\"\n\nif [[ \"\\${APP_BUILD_ENABLED}\" != \"true\" ]]; then\n echo \"Skipping app image build (APP_BUILD_ENABLED=\\${APP_BUILD_ENABLED}).\"\n exit 0\nfi\n\nif ! command -v docker >/dev/null 2>&1; then\n echo \"docker is required to build app image.\"\n exit 1\nfi\n\nif ! command -v bunx >/dev/null 2>&1; then\n echo \"bunx is required to export the app for container image build.\"\n exit 1\nfi\n\nif [[ ! -f \"\\${APP_SOURCE_DIR}/package.json\" ]]; then\n echo \"APP_SOURCE_DIR does not look like an app root: \\${APP_SOURCE_DIR}\"\n exit 1\nfi\n\nif [[ ! -f \"\\${DOCKERFILE_PATH}\" ]]; then\n echo \"Dockerfile for app image build is missing: \\${DOCKERFILE_PATH}\"\n exit 1\nfi\n\n(\n cd \"\\${APP_SOURCE_DIR}\"\n bunx expo export --platform web --output-dir \"\\${APP_WEB_EXPORT_DIR}\"\n)\n\nif [[ \"\\${APP_WEB_EXPORT_DIR}\" = /* ]]; then\n EXPORT_DIR=\"\\${APP_WEB_EXPORT_DIR}\"\nelse\n EXPORT_DIR=\"\\${APP_SOURCE_DIR}/\\${APP_WEB_EXPORT_DIR}\"\nfi\n\nif [[ ! -f \"\\${EXPORT_DIR}/index.html\" ]]; then\n echo \"Expected web export output not found at \\${EXPORT_DIR}/index.html\"\n exit 1\nfi\n\ndocker build -t \"\\${APP_IMAGE}\" \\\n --label \"ankhorage.kind=generated-app\" \\\n --label \"ankhorage.namespace=\\${NAMESPACE}\" \\\n --label \"ankhorage.image=\\${APP_IMAGE}\" \\\n -f \"\\${DOCKERFILE_PATH}\" \\\n \"\\${EXPORT_DIR}\"\necho \"Built app image: \\${APP_IMAGE} (namespace: \\${NAMESPACE})\"\n`;\n}\n\nfunction getAppImageDockerfile(): string {\n return `FROM nginx:1.27-alpine\n\nRUN cat <<'EOF' > /etc/nginx/conf.d/default.conf\nserver {\n listen 8080;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n\n location / {\n try_files $uri $uri/ /index.html;\n }\n}\nEOF\n\nCOPY . /usr/share/nginx/html\n\nEXPOSE 8080\n\nCMD [\"nginx\", \"-g\", \"daemon off;\"]\n`;\n}\n\nfunction getPortForwardScript(defaultNamespace: string): string {\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nNAMESPACE=\"\\${ANKH_NAMESPACE:-${defaultNamespace}}\"\nLOCAL_PORT=\"\\${APP_PORT_FORWARD_LOCAL_PORT:-18080}\"\nREMOTE_PORT=\"\\${APP_PORT_FORWARD_REMOTE_PORT:-80}\"\n\nif ! command -v kubectl >/dev/null 2>&1; then\n echo \"kubectl is required but not installed.\"\n exit 1\nfi\n\nif ! kubectl get namespace \"\\${NAMESPACE}\" >/dev/null 2>&1; then\n echo \"Namespace '\\${NAMESPACE}' was not found.\"\n echo \"Run ./scripts/up.sh to create infrastructure first.\"\n exit 1\nfi\n\nif ! kubectl -n \"\\${NAMESPACE}\" get service app-runtime >/dev/null 2>&1; then\n echo \"Service 'app-runtime' was not found in namespace '\\${NAMESPACE}'.\"\n echo \"Run ./scripts/up.sh to apply runtime resources.\"\n exit 1\nfi\n\necho \"Forwarding app-runtime from namespace '\\${NAMESPACE}' to http://127.0.0.1:\\${LOCAL_PORT}\"\necho \"Press Ctrl+C to stop.\"\nkubectl -n \"\\${NAMESPACE}\" port-forward service/app-runtime \"\\${LOCAL_PORT}:\\${REMOTE_PORT}\"\n`;\n}\n\nfunction getDownScript(): string {\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\nK8S_DIR=\"\\${ROOT_DIR}/k8s\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nkubectl delete -k \"\\${K8S_DIR}\" --ignore-not-found\necho \"Minikube infrastructure removed.\"\n`;\n}\n\nfunction getSupabaseProfileChecksumSql(profileModel: ResolvedProfileModel): string {\n return `do $$\ndeclare\n actual_hash text;\n actual_table_name text;\nbegin\n if to_regclass('ankhorage_internal.generated_schema_state') is null then\n raise exception 'missing ankhorage_internal.generated_schema_state';\n end if;\n\n select content_hash, table_name\n into actual_hash, actual_table_name\n from ankhorage_internal.generated_schema_state\n where artifact_key = 'auth.profile';\n\n if actual_hash is null then\n raise exception 'missing generated schema state for auth.profile';\n end if;\n\n if actual_table_name <> '${escapeSqlLiteral(profileModel.table)}' then\n raise exception 'generated schema state table mismatch for auth.profile: expected %, found %', '${escapeSqlLiteral(profileModel.table)}', actual_table_name;\n end if;\n\n if actual_hash <> '${escapeSqlLiteral(profileModel.desiredStateHash)}' then\n raise exception 'stale generated schema state for auth.profile: expected %, found %', '${escapeSqlLiteral(profileModel.desiredStateHash)}', actual_hash;\n end if;\nend;\n$$;`;\n}\n\nfunction getSupabaseProfileDisabledGuardSql(): string {\n return `do $$\nbegin\n if to_regclass('ankhorage_internal.generated_schema_state') is not null\n and exists (\n select 1\n from ankhorage_internal.generated_schema_state\n where artifact_key = 'auth.profile'\n ) then\n raise exception 'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration';\n end if;\nend;\n$$;`;\n}\n\nfunction getSupabaseProfileVerificationSql(profileModel: ResolvedProfileModel): string {\n const configuredColumns = profileModel.columns.map((column) => column.column);\n const configuredArray = getSqlTextArray(configuredColumns);\n const managedArray = getSqlTextArray(MANAGED_PROFILE_COLUMNS);\n const updateGrantArray = getSqlTextArray(configuredColumns);\n const protectedUpdateColumnsArray = getSqlTextArray(['id', 'created_at', 'updated_at', 'role']);\n const table = escapeSqlLiteral(profileModel.table);\n const selectPolicy = escapeSqlLiteral(`${profileModel.table}_select_own`);\n const updatePolicy = escapeSqlLiteral(`${profileModel.table}_update_own`);\n const triggerName = escapeSqlLiteral(`on_auth_user_created_${profileModel.table}`);\n const functionName = escapeSqlLiteral(`handle_new_${profileModel.table}_user`);\n const triggerCheck =\n profileModel.createStrategy === 'trigger'\n ? ` if not exists (\n select 1\n from pg_trigger t\n join pg_class rel on rel.oid = t.tgrelid\n join pg_namespace rel_ns on rel_ns.oid = rel.relnamespace\n join pg_proc fn on fn.oid = t.tgfoid\n join pg_namespace fn_ns on fn_ns.oid = fn.pronamespace\n where rel_ns.nspname = 'auth'\n and rel.relname = 'users'\n and t.tgname = '${triggerName}'\n and not t.tgisinternal\n and t.tgenabled = 'O'\n and (t.tgtype::integer & 1) = 1\n and (t.tgtype::integer & 4) = 4\n and (t.tgtype::integer & (2 | 8 | 16 | 32)) = 0\n and fn_ns.nspname = 'public'\n and fn.proname = '${functionName}'\n ) then\n raise exception 'missing generated new-user profile trigger';\n end if;`\n : ` if exists (\n select 1\n from pg_trigger t\n join pg_class rel on rel.oid = t.tgrelid\n join pg_namespace rel_ns on rel_ns.oid = rel.relnamespace\n where rel_ns.nspname = 'auth'\n and rel.relname = 'users'\n and t.tgname = '${triggerName}'\n and not t.tgisinternal\n ) then\n raise exception 'generated new-user profile trigger exists but createStrategy is not trigger';\n end if;`;\n\n return `do $$\ndeclare\n profile_table constant text := '${table}';\n configured_columns constant text[] := ${configuredArray};\n managed_columns constant text[] := ${managedArray};\n expected_column text;\nbegin\n if to_regclass('auth.users') is null then\n raise exception 'missing auth.users';\n end if;\n\n if to_regclass('public.users') is not null then\n raise exception 'reserved conflicting identity table public.users exists';\n end if;\n\n if to_regclass(format('public.%I', profile_table)) is null then\n raise exception 'missing configured profile table public.%', profile_table;\n end if;\n\n if not exists (\n select 1\n from pg_constraint c\n where c.conrelid = format('public.%I', profile_table)::regclass\n and c.contype = 'p'\n and c.conkey = array[\n (\n select a.attnum\n from pg_attribute a\n where a.attrelid = format('public.%I', profile_table)::regclass\n and a.attname = 'id'\n and not a.attisdropped\n )\n ]::smallint[]\n ) then\n raise exception 'profile table primary key must be exactly id';\n end if;\n\n if not exists (\n select 1\n from pg_constraint c\n where c.conrelid = format('public.%I', profile_table)::regclass\n and c.confrelid = 'auth.users'::regclass\n and c.contype = 'f'\n and c.confdeltype = 'c'\n and c.conkey = array[\n (\n select a.attnum\n from pg_attribute a\n where a.attrelid = format('public.%I', profile_table)::regclass\n and a.attname = 'id'\n and not a.attisdropped\n )\n ]::smallint[]\n and c.confkey = array[\n (\n select a.attnum\n from pg_attribute a\n where a.attrelid = 'auth.users'::regclass\n and a.attname = 'id'\n and not a.attisdropped\n )\n ]::smallint[]\n ) then\n raise exception 'profile id must reference auth.users(id) with cascade delete';\n end if;\n\n if not exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = 'id'\n and c.udt_name = 'uuid'\n and c.is_nullable = 'NO'\n ) then\n raise exception 'profile id column must be uuid not null';\n end if;\n\n if not exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = 'created_at'\n and c.data_type = 'timestamp with time zone'\n and c.is_nullable = 'NO'\n and c.column_default = 'now()'\n ) then\n raise exception 'profile created_at column must be timestamptz not null default now()';\n end if;\n\n if not exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = 'updated_at'\n and c.data_type = 'timestamp with time zone'\n and c.is_nullable = 'NO'\n and c.column_default = 'now()'\n ) then\n raise exception 'profile updated_at column must be timestamptz not null default now()';\n end if;\n\n foreach expected_column in array configured_columns loop\n if not exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = expected_column\n and c.data_type = 'text'\n ) then\n raise exception 'missing configured managed profile column %', expected_column;\n end if;\n end loop;\n\n foreach expected_column in array managed_columns loop\n if not expected_column = any(configured_columns) and exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = expected_column\n ) then\n raise exception 'stale managed profile column % exists', expected_column;\n end if;\n end loop;\n\n if exists (\n select 1\n from information_schema.columns\n where table_schema = 'public'\n and table_name = profile_table\n and column_name = 'role'\n ) then\n raise exception 'generated role column must not exist';\n end if;\n\n if not exists (\n select 1\n from pg_class c\n join pg_namespace n on n.oid = c.relnamespace\n where n.nspname = 'public'\n and c.relname = profile_table\n and c.relrowsecurity\n ) then\n raise exception 'profile table RLS is not enabled';\n end if;\n\n if not exists (\n select 1\n from pg_policy p\n join pg_class c on c.oid = p.polrelid\n join pg_namespace n on n.oid = c.relnamespace\n where n.nspname = 'public'\n and c.relname = profile_table\n and p.polname = '${selectPolicy}'\n and p.polcmd = 'r'\n and p.polroles = array['authenticated'::regrole]::oid[]\n and regexp_replace(lower(pg_get_expr(p.polqual, p.polrelid)), '[[:space:]()]', '', 'g') in (\n 'auth.uid=id',\n 'auth.uid=' || profile_table || '.id',\n 'selectauth.uidasuid=id',\n 'selectauth.uidasuid=' || profile_table || '.id'\n )\n and p.polwithcheck is null\n ) then\n raise exception 'own-profile SELECT policy is missing or has unsafe definition';\n end if;\n\n if not exists (\n select 1\n from pg_policy p\n join pg_class c on c.oid = p.polrelid\n join pg_namespace n on n.oid = c.relnamespace\n where n.nspname = 'public'\n and c.relname = profile_table\n and p.polname = '${updatePolicy}'\n and p.polcmd = 'w'\n and p.polroles = array['authenticated'::regrole]::oid[]\n and regexp_replace(lower(pg_get_expr(p.polqual, p.polrelid)), '[[:space:]()]', '', 'g') in (\n 'auth.uid=id',\n 'auth.uid=' || profile_table || '.id',\n 'selectauth.uidasuid=id',\n 'selectauth.uidasuid=' || profile_table || '.id'\n )\n and regexp_replace(lower(pg_get_expr(p.polwithcheck, p.polrelid)), '[[:space:]()]', '', 'g') in (\n 'auth.uid=id',\n 'auth.uid=' || profile_table || '.id',\n 'selectauth.uidasuid=id',\n 'selectauth.uidasuid=' || profile_table || '.id'\n )\n ) then\n raise exception 'own-profile UPDATE policy is missing or has unsafe definition';\n end if;\n\n if exists (\n select 1\n from pg_policy p\n join pg_class c on c.oid = p.polrelid\n join pg_namespace n on n.oid = c.relnamespace\n where n.nspname = 'public'\n and c.relname = profile_table\n and p.polname not in ('${selectPolicy}', '${updatePolicy}')\n ) then\n raise exception 'unexpected profile table RLS policy exists';\n end if;\n\n if has_any_column_privilege('anon', format('public.%I', profile_table), 'SELECT')\n or has_any_column_privilege('anon', format('public.%I', profile_table), 'INSERT')\n or has_any_column_privilege('anon', format('public.%I', profile_table), 'UPDATE')\n or has_any_column_privilege('anon', format('public.%I', profile_table), 'REFERENCES')\n or has_table_privilege('anon', format('public.%I', profile_table), 'DELETE') then\n raise exception 'anon must not have profile table privileges';\n end if;\n\n if not has_table_privilege('authenticated', format('public.%I', profile_table), 'SELECT') then\n raise exception 'authenticated role must have profile table SELECT privilege';\n end if;\n\n if has_any_column_privilege('authenticated', format('public.%I', profile_table), 'INSERT')\n or has_any_column_privilege('authenticated', format('public.%I', profile_table), 'REFERENCES')\n or has_table_privilege('authenticated', format('public.%I', profile_table), 'DELETE') then\n raise exception 'authenticated role must not have profile table INSERT, DELETE, or REFERENCES privilege';\n end if;\n\n foreach expected_column in array ${updateGrantArray} loop\n if not has_column_privilege('authenticated', format('public.%I', profile_table), expected_column, 'UPDATE') then\n raise exception 'authenticated role is missing UPDATE privilege on configured profile column %', expected_column;\n end if;\n end loop;\n\n foreach expected_column in array (\n select array_agg(c.column_name::text order by c.ordinal_position)\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n ) loop\n if not expected_column = any(configured_columns)\n and has_column_privilege('authenticated', format('public.%I', profile_table), expected_column, 'UPDATE') then\n raise exception 'authenticated role has unexpected UPDATE privilege on profile column %', expected_column;\n end if;\n end loop;\n\n foreach expected_column in array ${protectedUpdateColumnsArray} loop\n if exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = expected_column\n ) and has_column_privilege('authenticated', format('public.%I', profile_table), expected_column, 'UPDATE') then\n raise exception 'authenticated role must not have UPDATE privilege on protected profile column %', expected_column;\n end if;\n end loop;\n\n${triggerCheck}\n\n if '${profileModel.createStrategy}' = 'trigger' then\n if not exists (\n select 1\n from pg_proc p\n join pg_namespace n on n.oid = p.pronamespace\n where n.nspname = 'public'\n and p.proname = '${functionName}'\n and p.prosecdef\n and p.prorettype = 'trigger'::regtype\n and p.proconfig @> array['search_path=pg_catalog, pg_temp']\n ) then\n raise exception 'generated trigger function is missing required SECURITY DEFINER properties';\n end if;\n\n if exists (\n select 1\n from pg_proc p\n join pg_namespace n on n.oid = p.pronamespace\n cross join lateral aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) acl\n where n.nspname = 'public'\n and p.proname = '${functionName}'\n and acl.privilege_type = 'EXECUTE'\n and (\n acl.grantee = 0\n or acl.grantee = 'anon'::regrole\n or acl.grantee = 'authenticated'::regrole\n )\n ) then\n raise exception 'generated trigger function execute privilege must be revoked from PUBLIC, anon, and authenticated';\n end if;\n end if;\nend;\n$$;`;\n}\n\nfunction getStatusScript(args: {\n defaultNamespace: string;\n supabaseProjectId: string | null;\n supabaseLocalEnabled: boolean;\n profileModel: ResolvedProfileModel;\n}): string {\n const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;\n const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';\n const profileVerificationSql = profileModel.enabled\n ? getSupabaseProfileVerificationSql(profileModel)\n : '';\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nNAMESPACE=\"\\${ANKH_NAMESPACE:-${defaultNamespace}}\"\nAUTH_RUNTIME_MODE=\"\\${AUTH_RUNTIME_MODE:-local}\"\nSUPABASE_LOCAL_ENABLED=\"\\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}\"\nSUPABASE_SECRET_SYNC_ENABLED=\"\\${SUPABASE_SECRET_SYNC_ENABLED:-true}\"\nSUPABASE_PROJECT_DIR=\"\\${SUPABASE_PROJECT_DIR:-\\${ROOT_DIR}}\"\nEXPECTED_SUPABASE_PROJECT_ID=\"${supabaseProjectId ?? ''}\"\nSUPABASE_PROFILE_ENABLED=\"${profileModel.enabled ? 'true' : 'false'}\"\nSUPABASE_PROFILE_RECONCILE_FILE=\"\\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql\"\nexport EXPECTED_SUPABASE_PROJECT_ID\n\nreject_supabase_project_id_override() {\n if [[ -n \"\\${SUPABASE_PROJECT_ID:-}\" ]]; then\n echo \"SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.\"\n echo \"Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead.\"\n return 1\n fi\n\n unset SUPABASE_PROJECT_ID\n}\n\nrequire_expected_supabase_project_identity() {\n if [[ -z \"\\${EXPECTED_SUPABASE_PROJECT_ID}\" ]]; then\n echo \"Cannot run local Supabase infrastructure: expected Supabase project identity is empty.\"\n echo \"Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services.\"\n return 1\n fi\n}\n\nif ! command -v kubectl >/dev/null 2>&1; then\n echo \"kubectl is required but not installed.\"\n exit 1\nfi\n\nif ! kubectl config current-context >/dev/null 2>&1; then\n echo \"No active kubectl context is configured.\"\n echo \"Run ./scripts/up.sh to start Minikube and apply manifests.\"\n exit 1\nfi\n\nif ! kubectl cluster-info >/dev/null 2>&1; then\n CONTEXT=\"$(kubectl config current-context 2>/dev/null || echo \"unknown\")\"\n echo \"Cannot reach Kubernetes API server for context '\\${CONTEXT}'.\"\n echo \"Run ./scripts/up.sh or verify your kubeconfig context.\"\n exit 1\nfi\n\nif ! kubectl get namespace \"\\${NAMESPACE}\" >/dev/null 2>&1; then\n echo \"Namespace '\\${NAMESPACE}' was not found in the current cluster.\"\n echo \"Run ./scripts/up.sh to create it and apply resources.\"\n exit 1\nfi\n\nkubectl get all -n \"\\${NAMESPACE}\"\n\necho\necho \"Runtime auth checks:\"\necho \"- AUTH_RUNTIME_MODE=\\${AUTH_RUNTIME_MODE}\"\necho \"- SUPABASE_SECRET_SYNC_ENABLED=\\${SUPABASE_SECRET_SYNC_ENABLED}\"\n\nif kubectl -n \"\\${NAMESPACE}\" get configmap app-runtime-auth-env >/dev/null 2>&1; then\n echo \"- configmap/app-runtime-auth-env: present\"\nelse\n echo \"- configmap/app-runtime-auth-env: missing\"\nfi\n\nif [[ \"\\${SUPABASE_SECRET_SYNC_ENABLED}\" == \"true\" ]]; then\n if kubectl -n \"\\${NAMESPACE}\" get secret supabase-auth-secrets >/dev/null 2>&1; then\n echo \"- secret/supabase-auth-secrets: present\"\n\n has_secret_key() {\n local key=\"$1\"\n local value\n value=\"$(kubectl -n \"\\${NAMESPACE}\" get secret supabase-auth-secrets -o \"jsonpath={.data.\\${key}}\" 2>/dev/null || true)\"\n [[ -n \"\\${value}\" ]]\n }\n\n required_secret_keys=(\n SUPABASE_URL\n SUPABASE_ANON_KEY\n SUPABASE_SERVICE_ROLE_KEY\n SUPABASE_JWT_SECRET\n EXPO_PUBLIC_SUPABASE_URL\n EXPO_PUBLIC_SUPABASE_ANON_KEY\n )\n\n for key in \"\\${required_secret_keys[@]}\"; do\n if has_secret_key \"\\${key}\"; then\n echo \" - \\${key}: present\"\n else\n echo \" - \\${key}: missing\"\n fi\n done\n else\n echo \"- secret/supabase-auth-secrets: missing\"\n fi\nelse\n echo \"- secret/supabase-auth-secrets: skipped (SUPABASE_SECRET_SYNC_ENABLED=false)\"\nfi\n\nversion_ge() {\n local actual=\"$1\"\n local minimum=\"$2\"\n\n local actual_major actual_minor actual_patch\n local minimum_major minimum_minor minimum_patch\n IFS=. read -r actual_major actual_minor actual_patch <<< \"\\${actual}\"\n IFS=. read -r minimum_major minimum_minor minimum_patch <<< \"\\${minimum}\"\n\n actual_major=\"\\${actual_major:-0}\"\n actual_minor=\"\\${actual_minor:-0}\"\n actual_patch=\"\\${actual_patch:-0}\"\n minimum_major=\"\\${minimum_major:-0}\"\n minimum_minor=\"\\${minimum_minor:-0}\"\n minimum_patch=\"\\${minimum_patch:-0}\"\n\n (( actual_major > minimum_major )) && return 0\n (( actual_major < minimum_major )) && return 1\n (( actual_minor > minimum_minor )) && return 0\n (( actual_minor < minimum_minor )) && return 1\n (( actual_patch >= minimum_patch ))\n}\n\nrequire_supabase_cli_capabilities() {\n if ! command -v supabase >/dev/null 2>&1; then\n echo \"supabase CLI is required for local schema status checks.\"\n echo \"Install or upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n return 1\n fi\n\n local version\n version=\"$(supabase --version | awk '{print $NF}')\"\n if ! version_ge \"\\${version}\" \"2.106.0\"; then\n echo \"supabase CLI >= 2.106.0 is required; found \\${version}.\"\n echo \"Upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n return 1\n fi\n\n if ! supabase status --help 2>/dev/null | grep -Fq -- \"--workdir\"; then\n echo \"supabase CLI does not support required global --workdir flag.\"\n return 1\n fi\n\n if ! supabase migration up --help 2>/dev/null | grep -Fq -- \"--local\"; then\n echo \"supabase CLI does not support required migration up --local command.\"\n return 1\n fi\n\n if ! command -v psql >/dev/null 2>&1; then\n echo \"psql is required for local schema status checks.\"\n return 1\n fi\n}\n\nread_env_value() {\n local source=\"$1\"\n local key=\"$2\"\n local line\n line=\"$(printf '%s\\\\n' \"\\${source}\" | awk -v key=\"\\${key}\" '\n $0 ~ \"^(export[[:space:]]+)?\" key \"=\" {print}\n ' | tail -n1)\"\n\n if [[ -z \"\\${line}\" ]]; then\n echo \"\"\n return\n fi\n\n line=\"\\${line#export }\"\n line=\"\\${line#\\${key}=}\"\n line=\"\\${line%\\\\\"}\"\n line=\"\\${line#\\\\\"}\"\n printf '%s' \"\\${line}\"\n}\n\nrun_supabase_sql() {\n local sql=\"$1\"\n local label=\"$2\"\n local tmp_file\n tmp_file=\"$(mktemp)\"\n printf '%s\\\\n' \"\\${sql}\" > \"\\${tmp_file}\"\n\n if psql \"\\${SUPABASE_DB_URL}\" -v ON_ERROR_STOP=1 -q -f \"\\${tmp_file}\" >/dev/null; then\n rm -f \"\\${tmp_file}\"\n return 0\n fi\n\n rm -f \"\\${tmp_file}\"\n echo \"- \\${label}: failed\"\n return 1\n}\n\nvalidate_supabase_project_identity() {\n local config_file=\"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\"\n\n if [[ -z \"\\${EXPECTED_SUPABASE_PROJECT_ID}\" ]]; then\n return 0\n fi\n\n if [[ ! -f \"\\${config_file}\" ]]; then\n echo \"- local Supabase project identity: config missing at \\${config_file}\"\n return 1\n fi\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \"- local Supabase project identity: python3 is required to read \\${config_file}\"\n return 1\n fi\n\n python3 - \"\\${config_file}\" <<'PY'\nimport os\nimport re\nimport sys\n\n\ndef read_top_level_project_id(config_path):\n key_re = re.compile(r'^\\\\s*project_id\\\\s*=\\\\s*(.*?)\\\\s*(?:#.*)?$')\n section_re = re.compile(r'^\\\\s*\\\\[[^\\\\]]+\\\\]\\\\s*$')\n\n with open(config_path, 'r', encoding='utf-8') as f:\n for line in f:\n if section_re.match(line):\n return None\n match = key_re.match(line)\n if not match:\n continue\n raw = match.group(1).strip()\n quoted = re.match(r'^[\"\\\\']([^\"\\\\']*)[\"\\\\']$', raw)\n return quoted.group(1) if quoted else raw\n\n return None\n\n\nexpected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']\nactual = read_top_level_project_id(sys.argv[1])\n\nif actual == expected:\n raise SystemExit(0)\n\nfound = 'missing' if actual is None else f'\"{actual}\"'\nprint('Supabase project identity mismatch.', file=sys.stderr)\nprint(f'Expected \"{expected}\", found {found}.', file=sys.stderr)\nprint('Destroy the invalid local stack and run Infra Up again.', file=sys.stderr)\nraise SystemExit(1)\nPY\n}\n\ncheck_immutable_migrations_applied() {\n local migrations_dir=\"\\${SUPABASE_PROJECT_DIR}/supabase/migrations\"\n local applied_versions\n applied_versions=\"$(psql \"\\${SUPABASE_DB_URL}\" -v ON_ERROR_STOP=1 -Atc \"select version from supabase_migrations.schema_migrations\" 2>/dev/null || true)\"\n\n if [[ -z \"\\${applied_versions}\" ]]; then\n applied_versions=\"\"\n fi\n\n if [[ ! -d \"\\${migrations_dir}\" ]]; then\n return 0\n fi\n\n local file\n local version\n local pending=0\n for file in \"\\${migrations_dir}\"/*.sql; do\n [[ -e \"\\${file}\" ]] || continue\n version=\"$(basename \"\\${file}\")\"\n version=\"\\${version%%_*}\"\n if ! printf '%s\\\\n' \"\\${applied_versions}\" | grep -Fxq \"\\${version}\"; then\n echo \"- immutable migrations: pending \\${file}\"\n pending=1\n fi\n done\n\n return \"\\${pending}\"\n}\n\nif [[ \"\\${AUTH_RUNTIME_MODE}\" == \"local\" && \"\\${SUPABASE_LOCAL_ENABLED}\" == \"true\" ]]; then\n echo\n echo \"Local Supabase database checks:\"\n status_failed=0\n\n if ! require_expected_supabase_project_identity; then\n status_failed=1\n elif ! reject_supabase_project_id_override; then\n status_failed=1\n elif require_supabase_cli_capabilities; then\n if ! validate_supabase_project_identity; then\n status_failed=1\n elif supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status >/dev/null 2>&1; then\n STATUS_ENV=\"$(supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status -o env)\"\n SUPABASE_DB_URL=\"$(read_env_value \"\\${STATUS_ENV}\" DB_URL)\"\n if [[ -z \"\\${SUPABASE_DB_URL}\" ]]; then\n echo \"- immutable migrations: status unavailable (DB_URL missing from supabase status)\"\n status_failed=1\n elif check_immutable_migrations_applied; then\n echo \"- immutable migrations: applied\"\n else\n echo \"- immutable migrations: pending\"\n status_failed=1\n fi\n\n if [[ \"\\${SUPABASE_PROFILE_ENABLED}\" == \"true\" ]]; then\n if [[ -f \"\\${SUPABASE_PROFILE_RECONCILE_FILE}\" ]]; then\n PROFILE_STATUS_SQL=$(cat <<'SQL'\n${profileStatusSql}\nSQL\n)\n PROFILE_VERIFY_SQL=$(cat <<'SQL'\n${profileVerificationSql}\nSQL\n)\n\n if run_supabase_sql \"\\${PROFILE_STATUS_SQL}\" \"profile reconciliation\"; then\n echo \"- profile reconciliation: applied, checksum matches\"\n else\n echo \"- profile reconciliation: pending or stale\"\n status_failed=1\n fi\n\n if run_supabase_sql \"\\${PROFILE_VERIFY_SQL}\" \"profile schema\"; then\n echo \"- profile schema: verified\"\n else\n echo \"- profile schema: drift detected\"\n status_failed=1\n fi\n else\n echo \"- profile reconciliation: generated file missing at \\${SUPABASE_PROFILE_RECONCILE_FILE}\"\n echo \"- profile schema: not verified\"\n status_failed=1\n fi\n else\n PROFILE_DISABLED_SQL=$(cat <<'SQL'\n${getSupabaseProfileDisabledGuardSql()}\nSQL\n)\n if run_supabase_sql \"\\${PROFILE_DISABLED_SQL}\" \"profile disabled-state\"; then\n echo \"- profile reconciliation: skipped (no profile table configured)\"\n echo \"- profile schema: skipped (no profile table configured)\"\n else\n echo \"- profile reconciliation: stale generated state exists for removed profile table\"\n echo \"- profile schema: not verified\"\n status_failed=1\n fi\n fi\n else\n echo \"- local Supabase stack: not running or not reachable from \\${SUPABASE_PROJECT_DIR}\"\n status_failed=1\n fi\n else\n status_failed=1\n fi\n\n if [[ \"\\${status_failed}\" -ne 0 ]]; then\n exit \"\\${status_failed}\"\n fi\nfi\n`;\n}\n\nfunction getSupabaseLocalEnvScript(args: {\n supabaseProjectId: string | null;\n supabaseLocalPorts: SupabaseLocalPorts;\n profileModel: ResolvedProfileModel;\n}): string {\n const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;\n const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';\n const profileVerificationSql = profileModel.enabled\n ? getSupabaseProfileVerificationSql(profileModel)\n : '';\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\nENV_FILE=\"\\${ROOT_DIR}/.env\"\nENV_EXAMPLE_FILE=\"\\${ROOT_DIR}/.env.example\"\n\nif [[ -f \"\\${ENV_FILE}\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ENV_FILE}\"\n set +a\nfi\n\nAPP_SOURCE_DIR=\"\\${APP_SOURCE_DIR:-$(cd \"\\${ROOT_DIR}/../..\" && pwd)}\"\nSUPABASE_PROJECT_DIR=\"\\${SUPABASE_PROJECT_DIR:-\\${ROOT_DIR}}\"\nEXPECTED_SUPABASE_PROJECT_ID=\"${supabaseProjectId ?? ''}\"\nAPP_SUPABASE_ENV_FILE=\"\\${APP_SUPABASE_ENV_FILE:-\\${APP_SOURCE_DIR}/.env.local}\"\nSUPABASE_PROFILE_ENABLED=\"${profileModel.enabled ? 'true' : 'false'}\"\nSUPABASE_PROFILE_RECONCILE_FILE=\"\\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql\"\nexport EXPECTED_SUPABASE_PROJECT_ID\n\nreject_supabase_project_id_override() {\n if [[ -n \"\\${SUPABASE_PROJECT_ID:-}\" ]]; then\n echo \"SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.\"\n echo \"Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead.\"\n exit 1\n fi\n\n unset SUPABASE_PROJECT_ID\n}\n\nrequire_expected_supabase_project_identity() {\n if [[ -z \"\\${EXPECTED_SUPABASE_PROJECT_ID}\" ]]; then\n echo \"Cannot run local Supabase infrastructure: expected Supabase project identity is empty.\"\n echo \"Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services.\"\n exit 1\n fi\n}\n\nSUPABASE_LOCAL_PORT_BASE=\"\\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}\"\nSUPABASE_LOCAL_SHADOW_PORT=\"\\${SUPABASE_LOCAL_SHADOW_PORT:-\\${SUPABASE_LOCAL_PORT_BASE}}\"\nSUPABASE_LOCAL_API_PORT=\"\\${SUPABASE_LOCAL_API_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 1))}\"\nSUPABASE_LOCAL_DB_PORT=\"\\${SUPABASE_LOCAL_DB_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 2))}\"\nSUPABASE_LOCAL_STUDIO_PORT=\"\\${SUPABASE_LOCAL_STUDIO_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 3))}\"\nSUPABASE_LOCAL_INBUCKET_PORT=\"\\${SUPABASE_LOCAL_INBUCKET_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 4))}\"\nSUPABASE_LOCAL_ANALYTICS_PORT=\"\\${SUPABASE_LOCAL_ANALYTICS_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 5))}\"\nexport SUPABASE_LOCAL_PORT_BASE\nexport SUPABASE_LOCAL_SHADOW_PORT\nexport SUPABASE_LOCAL_API_PORT\nexport SUPABASE_LOCAL_DB_PORT\nexport SUPABASE_LOCAL_STUDIO_PORT\nexport SUPABASE_LOCAL_INBUCKET_PORT\nexport SUPABASE_LOCAL_ANALYTICS_PORT\nversion_ge() {\n local actual=\"$1\"\n local minimum=\"$2\"\n\n local actual_major actual_minor actual_patch\n local minimum_major minimum_minor minimum_patch\n IFS=. read -r actual_major actual_minor actual_patch <<< \"\\${actual}\"\n IFS=. read -r minimum_major minimum_minor minimum_patch <<< \"\\${minimum}\"\n\n actual_major=\"\\${actual_major:-0}\"\n actual_minor=\"\\${actual_minor:-0}\"\n actual_patch=\"\\${actual_patch:-0}\"\n minimum_major=\"\\${minimum_major:-0}\"\n minimum_minor=\"\\${minimum_minor:-0}\"\n minimum_patch=\"\\${minimum_patch:-0}\"\n\n (( actual_major > minimum_major )) && return 0\n (( actual_major < minimum_major )) && return 1\n (( actual_minor > minimum_minor )) && return 0\n (( actual_minor < minimum_minor )) && return 1\n (( actual_patch >= minimum_patch ))\n}\n\nrequire_supabase_cli_capabilities() {\n if ! command -v supabase >/dev/null 2>&1; then\n echo \"supabase CLI is required but not installed.\"\n echo \"Install or upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n exit 1\n fi\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \"python3 is required to reconcile supabase/config.toml before local Supabase startup.\"\n exit 1\n fi\n\n local version\n version=\"$(supabase --version | awk '{print $NF}')\"\n if ! version_ge \"\\${version}\" \"2.106.0\"; then\n echo \"supabase CLI >= 2.106.0 is required; found \\${version}.\"\n echo \"Upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n exit 1\n fi\n\n if ! supabase status --help 2>/dev/null | grep -Fq -- \"--workdir\"; then\n echo \"supabase CLI does not support required global --workdir flag.\"\n echo \"Upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n exit 1\n fi\n\n if ! supabase migration up --help 2>/dev/null | grep -Fq -- \"--local\"; then\n echo \"supabase CLI does not support required migration up --local command.\"\n echo \"Upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n exit 1\n fi\n\n if ! command -v psql >/dev/null 2>&1; then\n echo \"psql is required but not installed.\"\n exit 1\n fi\n}\n\nformat_command() {\n printf '%q ' \"$@\"\n}\n\nrun_checked_command() {\n local label=\"$1\"\n local sql_file=\"$2\"\n shift 2\n local status\n set +e\n \"$@\"\n status=$?\n set -e\n\n if [[ \"\\${status}\" -ne 0 ]]; then\n echo \"\\${label} failed.\"\n if [[ -n \"\\${sql_file}\" ]]; then\n echo \"SQL file: \\${sql_file}\"\n fi\n echo \"Supabase project workdir: \\${SUPABASE_PROJECT_DIR}\"\n echo \"Command: $(format_command \"$@\")\"\n echo \"Exit status: \\${status}\"\n exit \"\\${status}\"\n fi\n}\n\nrun_checked_sql_file() {\n local label=\"$1\"\n local sql_file=\"$2\"\n run_checked_command \\\n \"\\${label}\" \\\n \"\\${sql_file}\" \\\n psql \"\\${SUPABASE_DB_URL}\" -v ON_ERROR_STOP=1 -q -f \"\\${sql_file}\"\n}\n\nrequire_expected_supabase_project_identity\nreject_supabase_project_id_override\nrequire_supabase_cli_capabilities\n\nif [[ ! -d \"\\${SUPABASE_PROJECT_DIR}\" ]]; then\n echo \"Supabase project directory does not exist: \\${SUPABASE_PROJECT_DIR}\"\n exit 1\nfi\n\nconfigure_supabase_local_ports() {\n local config_file=\"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\"\n\n python3 - \"\\${config_file}\" <<'PY'\nimport os\nimport re\nimport sys\nfrom dataclasses import dataclass\n\n\n@dataclass(frozen=True)\nclass Patch:\n section: str\n key: str\n value: str\n\n\ndef _env_required(name):\n value = os.environ.get(name)\n if value is None or value == \"\":\n raise RuntimeError(f\"Missing required env var: {name}\")\n return value\n\n\ndef _is_section_header(line):\n return re.match(r\"^\\\\s*\\\\[[^\\\\]]+\\\\]\\\\s*$\", line) is not None\n\n\ndef _section_name(line):\n match = re.match(r\"^\\\\s*\\\\[([^\\\\]]+)\\\\]\\\\s*$\", line)\n return match.group(1) if match else None\n\n\ndef _patch_section(lines, section, key, value):\n current_section = None\n section_start = None\n\n for i, line in enumerate(lines):\n if _is_section_header(line):\n current_section = _section_name(line)\n if current_section == section and section_start is None:\n section_start = i\n\n if section_start is None:\n if lines and not lines[-1].endswith(\"\\\\n\"):\n lines[-1] = lines[-1] + \"\\\\n\"\n if lines and lines[-1].strip() != \"\":\n lines.append(\"\\\\n\")\n lines.append(f\"[{section}]\\\\n\")\n lines.append(f\"{key} = {value}\\\\n\")\n return lines\n\n section_end = len(lines)\n for j in range(section_start + 1, len(lines)):\n if _is_section_header(lines[j]):\n section_end = j\n break\n\n key_re = re.compile(rf\"^(\\\\s*){re.escape(key)}\\\\s*=\\\\s*(.*?)(\\\\s*(#.*)?)?$\")\n for k in range(section_start + 1, section_end):\n match = key_re.match(lines[k])\n if match:\n indent = match.group(1) or \"\"\n trailing = match.group(3) or \"\"\n suffix = trailing.rstrip(\"\\\\n\")\n lines[k] = f\"{indent}{key} = {value}{suffix}\\\\n\"\n return lines\n\n insert_at = section_end\n while insert_at > section_start + 1 and lines[insert_at - 1].strip() == \"\":\n insert_at -= 1\n lines.insert(insert_at, f\"{key} = {value}\\\\n\")\n return lines\n\n\ndef main():\n if len(sys.argv) != 2:\n raise RuntimeError(\"Usage: patch_config.py <supabase/config.toml>\")\n\n config_path = sys.argv[1]\n with open(config_path, \"r\", encoding=\"utf-8\") as f:\n lines = f.readlines()\n\n patches = [\n Patch(\"api\", \"port\", _env_required(\"SUPABASE_LOCAL_API_PORT\")),\n Patch(\"db\", \"port\", _env_required(\"SUPABASE_LOCAL_DB_PORT\")),\n Patch(\"db\", \"shadow_port\", _env_required(\"SUPABASE_LOCAL_SHADOW_PORT\")),\n Patch(\"studio\", \"port\", _env_required(\"SUPABASE_LOCAL_STUDIO_PORT\")),\n Patch(\"inbucket\", \"port\", _env_required(\"SUPABASE_LOCAL_INBUCKET_PORT\")),\n Patch(\"analytics\", \"port\", _env_required(\"SUPABASE_LOCAL_ANALYTICS_PORT\")),\n ]\n\n for p in patches:\n lines = _patch_section(lines, p.section, p.key, p.value)\n\n with open(config_path, \"w\", encoding=\"utf-8\") as f:\n f.writelines(lines)\n\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\nPY\n}\n\nwrite_supabase_project_identity_for_new_config() {\n local config_file=\"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\"\n\n require_expected_supabase_project_identity\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \"python3 is required to write Supabase project_id in \\${config_file}.\"\n exit 1\n fi\n\n python3 - \"\\${config_file}\" <<'PY'\nimport os\nimport re\nimport sys\n\n\ndef write_top_level_project_id(config_path, expected):\n key_re = re.compile(r'^(\\\\s*)project_id\\\\s*=\\\\s*(.*?)(\\\\s*(#.*)?)?$')\n section_re = re.compile(r'^\\\\s*\\\\[[^\\\\]]+\\\\]\\\\s*$')\n\n with open(config_path, 'r', encoding='utf-8') as f:\n lines = f.readlines()\n\n section_start = len(lines)\n for i, line in enumerate(lines):\n if section_re.match(line):\n section_start = i\n break\n\n for i in range(section_start):\n match = key_re.match(lines[i])\n if not match:\n continue\n indent = match.group(1) or ''\n trailing = (match.group(3) or '').rstrip('\\\\n')\n lines[i] = f'{indent}project_id = \"{expected}\"{trailing}\\\\n'\n break\n else:\n insert_at = section_start\n lines.insert(insert_at, f'project_id = \"{expected}\"\\\\n')\n\n with open(config_path, 'w', encoding='utf-8') as f:\n f.writelines(lines)\n\n\nwrite_top_level_project_id(sys.argv[1], os.environ['EXPECTED_SUPABASE_PROJECT_ID'])\nPY\n}\n\nvalidate_supabase_project_identity() {\n local config_file=\"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\"\n\n require_expected_supabase_project_identity\n\n if [[ ! -f \"\\${config_file}\" ]]; then\n echo \"Supabase project identity mismatch.\"\n printf 'Expected \"%s\", found missing.\\n' \"\\${EXPECTED_SUPABASE_PROJECT_ID}\"\n echo \"The existing local Supabase project belongs to a different identity.\"\n echo \"Destroy the invalid local stack and its project-owned resources, then run Infra Up again.\"\n exit 1\n fi\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \"python3 is required to read Supabase project_id in \\${config_file}.\"\n exit 1\n fi\n\n python3 - \"\\${config_file}\" <<'PY'\nimport os\nimport re\nimport sys\n\n\ndef read_top_level_project_id(config_path):\n key_re = re.compile(r'^\\\\s*project_id\\\\s*=\\\\s*(.*?)\\\\s*(?:#.*)?$')\n section_re = re.compile(r'^\\\\s*\\\\[[^\\\\]]+\\\\]\\\\s*$')\n\n with open(config_path, 'r', encoding='utf-8') as f:\n for line in f:\n if section_re.match(line):\n return None\n match = key_re.match(line)\n if not match:\n continue\n raw = match.group(1).strip()\n quoted = re.match(r'^[\"\\\\']([^\"\\\\']*)[\"\\\\']$', raw)\n return quoted.group(1) if quoted else raw\n\n return None\n\n\nexpected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']\nactual = read_top_level_project_id(sys.argv[1])\n\nif actual == expected:\n raise SystemExit(0)\n\nfound = 'missing' if actual is None else f'\"{actual}\"'\nprint('Supabase project identity mismatch.', file=sys.stderr)\nprint(f'Expected \"{expected}\", found {found}.', file=sys.stderr)\nprint('The existing local Supabase project belongs to a different identity.', file=sys.stderr)\nprint('Destroy the invalid local stack and its project-owned resources, then run Infra Up again.', file=sys.stderr)\nraise SystemExit(1)\nPY\n}\n\nassert_supabase_local_ports_available() {\n local project_label=\"\\${EXPECTED_SUPABASE_PROJECT_ID}\"\n\n ANKH_PROJECT_LABEL=\"\\${project_label}\" python3 - <<'PY'\nimport os\nimport socket\nimport sys\n\n\nPORT_KEYS = [\n \"SUPABASE_LOCAL_SHADOW_PORT\",\n \"SUPABASE_LOCAL_API_PORT\",\n \"SUPABASE_LOCAL_DB_PORT\",\n \"SUPABASE_LOCAL_STUDIO_PORT\",\n \"SUPABASE_LOCAL_INBUCKET_PORT\",\n \"SUPABASE_LOCAL_ANALYTICS_PORT\",\n]\n\n\ndef read_port(key):\n value = os.environ.get(key, \"\")\n try:\n port = int(value)\n except ValueError:\n raise RuntimeError(f\"{key} must be an integer, got {value!r}\") from None\n\n if port < 1 or port > 65535:\n raise RuntimeError(f\"{key} must be between 1 and 65535, got {port}\")\n\n return port\n\n\ndef can_bind(port):\n with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:\n try:\n sock.bind((\"0.0.0.0\", port))\n except OSError:\n return False\n return True\n\n\ndef main():\n project_label = os.environ.get(\"ANKH_PROJECT_LABEL\", \"unknown\")\n ports = [(key, read_port(key)) for key in PORT_KEYS]\n\n seen = {}\n duplicates = []\n for key, port in ports:\n existing = seen.get(port)\n if existing is not None:\n duplicates.append((key, existing, port))\n else:\n seen[port] = key\n\n blocked = [(key, port) for key, port in ports if not can_bind(port)]\n\n if not duplicates and not blocked:\n return 0\n\n print(f\"Supabase local port preflight failed for project '{project_label}'.\", file=sys.stderr)\n\n for key, existing, port in duplicates:\n print(f\"- {key} duplicates {existing} on port {port}.\", file=sys.stderr)\n\n for key, port in blocked:\n print(f\"- {key}={port} is already in use on this host.\", file=sys.stderr)\n\n print(\"Override SUPABASE_LOCAL_PORT_BASE or the specific SUPABASE_LOCAL_*_PORT values in infra/minikube/.env and retry.\", file=sys.stderr)\n return 1\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\nPY\n}\n\nif [[ ! -f \"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\" ]]; then\n echo \"No supabase/config.toml found in \\${SUPABASE_PROJECT_DIR}. Initializing Supabase project...\"\n supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" init --yes >/dev/null\n write_supabase_project_identity_for_new_config\nfi\n\nvalidate_supabase_project_identity\nconfigure_supabase_local_ports\n\nSTATUS_ENV=\"$(\n supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status -o env 2>/dev/null || true\n)\"\n\nread_env_value() {\n local key=\"$1\"\n local line\n line=\"$(printf '%s\\\\n' \"\\${STATUS_ENV}\" | awk -v key=\"\\${key}\" '\n $0 ~ \"^(export[[:space:]]+)?\" key \"=\" {print}\n ' | tail -n1)\"\n\n if [[ -z \"\\${line}\" ]]; then\n echo \"\"\n return\n fi\n\n line=\"\\${line#export }\"\n line=\"\\${line#\\${key}=}\"\n line=\"\\${line%\\\\\"}\"\n line=\"\\${line#\\\\\"}\"\n printf '%s' \"\\${line}\"\n}\n\ncheck_immutable_migrations_applied() {\n local migrations_dir=\"\\${SUPABASE_PROJECT_DIR}/supabase/migrations\"\n local applied_versions\n applied_versions=\"$(psql \"\\${SUPABASE_DB_URL}\" -v ON_ERROR_STOP=1 -Atc \"select version from supabase_migrations.schema_migrations\" 2>/dev/null || true)\"\n\n if [[ -z \"\\${applied_versions}\" ]]; then\n applied_versions=\"\"\n fi\n\n if [[ ! -d \"\\${migrations_dir}\" ]]; then\n return 0\n fi\n\n local file\n local version\n local pending=0\n for file in \"\\${migrations_dir}\"/*.sql; do\n [[ -e \"\\${file}\" ]] || continue\n version=\"$(basename \"\\${file}\")\"\n version=\"\\${version%%_*}\"\n if ! printf '%s\\\\n' \"\\${applied_versions}\" | grep -Fxq \"\\${version}\"; then\n echo \"Pending immutable Supabase migration: \\${file}\"\n pending=1\n fi\n done\n\n return \"\\${pending}\"\n}\n\nstart_supabase_local_stack() {\n if supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" start >/dev/null; then\n return 0\n fi\n\n echo \"Supabase local start failed. Stopping stale local stack and retrying once...\"\n supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" stop --no-backup >/dev/null 2>&1 || true\n\n if supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" start >/dev/null; then\n return 0\n fi\n\n echo \"Supabase local start failed after retry.\"\n return 1\n}\n\nif [[ -z \"\\${STATUS_ENV}\" ]]; then\n validate_supabase_project_identity\n assert_supabase_local_ports_available\n echo \"Supabase local stack not detected. Starting with 'supabase start'...\"\n start_supabase_local_stack\n STATUS_ENV=\"$(supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status -o env)\"\nfi\n\nSUPABASE_DB_URL=\"$(read_env_value DB_URL)\"\nif [[ -z \"\\${SUPABASE_DB_URL}\" ]]; then\n echo \"Unable to read DB_URL from 'supabase status -o env'.\"\n exit 1\nfi\n\necho \"Applying pending immutable Supabase migrations...\"\nvalidate_supabase_project_identity\nrun_checked_command \\\n \"Immutable Supabase migration application\" \\\n \"\" \\\n supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" migration up --local\n\nif ! check_immutable_migrations_applied; then\n echo \"Immutable Supabase migration application did not apply every local migration.\"\n exit 1\nfi\n\nif [[ \"\\${SUPABASE_PROFILE_ENABLED}\" == \"true\" ]]; then\n if [[ ! -f \"\\${SUPABASE_PROFILE_RECONCILE_FILE}\" ]]; then\n echo \"Generated profile reconciliation file is missing: \\${SUPABASE_PROFILE_RECONCILE_FILE}\"\n exit 1\n fi\n\n echo \"Applying generated profile reconciliation...\"\n run_checked_sql_file \\\n \"Generated profile reconciliation\" \\\n \"\\${SUPABASE_PROFILE_RECONCILE_FILE}\"\n\n PROFILE_STATUS_SQL=$(cat <<'SQL'\n${profileStatusSql}\nSQL\n)\n PROFILE_VERIFY_SQL=$(cat <<'SQL'\n${profileVerificationSql}\nSQL\n)\n\n PROFILE_STATUS_FILE=\"$(mktemp)\"\n PROFILE_VERIFY_FILE=\"$(mktemp)\"\n printf '%s\\\\n' \"\\${PROFILE_STATUS_SQL}\" > \"\\${PROFILE_STATUS_FILE}\"\n printf '%s\\\\n' \"\\${PROFILE_VERIFY_SQL}\" > \"\\${PROFILE_VERIFY_FILE}\"\n\n run_checked_sql_file \\\n \"Generated profile reconciliation checksum verification\" \\\n \"\\${PROFILE_STATUS_FILE}\"\n run_checked_sql_file \\\n \"Generated profile schema verification\" \\\n \"\\${PROFILE_VERIFY_FILE}\"\n\n rm -f \"\\${PROFILE_STATUS_FILE}\" \"\\${PROFILE_VERIFY_FILE}\"\n echo \"Generated profile reconciliation applied and schema verified.\"\nelse\n PROFILE_DISABLED_FILE=\"$(mktemp)\"\n cat > \"\\${PROFILE_DISABLED_FILE}\" <<'SQL'\n${getSupabaseProfileDisabledGuardSql()}\nSQL\n run_checked_sql_file \\\n \"Generated profile disabled-state verification\" \\\n \"\\${PROFILE_DISABLED_FILE}\"\n rm -f \"\\${PROFILE_DISABLED_FILE}\"\nfi\n\nSTATUS_ENV=\"$(supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status -o env)\"\n\nAPI_URL=\"$(read_env_value API_URL)\"\nANON_KEY=\"$(read_env_value ANON_KEY)\"\nSERVICE_ROLE_KEY=\"$(read_env_value SERVICE_ROLE_KEY)\"\nJWT_SECRET=\"$(read_env_value JWT_SECRET)\"\n\nif [[ -z \"\\${API_URL}\" || -z \"\\${ANON_KEY}\" || -z \"\\${SERVICE_ROLE_KEY}\" || -z \"\\${JWT_SECRET}\" ]]; then\n echo \"Unable to read required Supabase values from 'supabase status -o env'.\"\n echo \"Expected API_URL, ANON_KEY, SERVICE_ROLE_KEY, JWT_SECRET.\"\n exit 1\nfi\n\nupsert_env() {\n local file=\"$1\"\n local key=\"$2\"\n local value=\"$3\"\n local tmp_file\n tmp_file=\"$(mktemp)\"\n\n awk -v key=\"\\${key}\" -v value=\"\\${value}\" '\n BEGIN { replaced = 0 }\n index($0, key \"=\") == 1 {\n print key \"=\" value\n replaced = 1\n next\n }\n { print }\n END {\n if (replaced == 0) {\n print key \"=\" value\n }\n }\n ' \"\\${file}\" > \"\\${tmp_file}\"\n\n mv \"\\${tmp_file}\" \"\\${file}\"\n}\n\nif [[ ! -f \"\\${ENV_FILE}\" ]]; then\n cp \"\\${ENV_EXAMPLE_FILE}\" \"\\${ENV_FILE}\"\nfi\n\nif [[ ! -f \"\\${APP_SUPABASE_ENV_FILE}\" ]]; then\n touch \"\\${APP_SUPABASE_ENV_FILE}\"\nfi\n\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_SECRET_SYNC_ENABLED\" \"true\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_URL\" \"\\${API_URL}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_ANON_KEY\" \"\\${ANON_KEY}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_SERVICE_ROLE_KEY\" \"\\${SERVICE_ROLE_KEY}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_JWT_SECRET\" \"\\${JWT_SECRET}\"\nupsert_env \"\\${ENV_FILE}\" \"EXPO_PUBLIC_SUPABASE_URL\" \"\\${API_URL}\"\nupsert_env \"\\${ENV_FILE}\" \"EXPO_PUBLIC_SUPABASE_ANON_KEY\" \"\\${ANON_KEY}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_PROJECT_DIR\" \"\\${SUPABASE_PROJECT_DIR}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_PORT_BASE\" \"\\${SUPABASE_LOCAL_PORT_BASE}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_SHADOW_PORT\" \"\\${SUPABASE_LOCAL_SHADOW_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_API_PORT\" \"\\${SUPABASE_LOCAL_API_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_DB_PORT\" \"\\${SUPABASE_LOCAL_DB_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_STUDIO_PORT\" \"\\${SUPABASE_LOCAL_STUDIO_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_INBUCKET_PORT\" \"\\${SUPABASE_LOCAL_INBUCKET_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_ANALYTICS_PORT\" \"\\${SUPABASE_LOCAL_ANALYTICS_PORT}\"\n\nupsert_env \"\\${APP_SUPABASE_ENV_FILE}\" \"EXPO_PUBLIC_SUPABASE_URL\" \"\\${API_URL}\"\nupsert_env \"\\${APP_SUPABASE_ENV_FILE}\" \"EXPO_PUBLIC_SUPABASE_ANON_KEY\" \"\\${ANON_KEY}\"\nupsert_env \"\\${APP_SUPABASE_ENV_FILE}\" \"SUPABASE_URL\" \"\\${API_URL}\"\nupsert_env \"\\${APP_SUPABASE_ENV_FILE}\" \"SUPABASE_ANON_KEY\" \"\\${ANON_KEY}\"\n\necho \"Updated \\${ENV_FILE} with local Supabase credentials from \\${SUPABASE_PROJECT_DIR}.\"\necho \"Updated \\${APP_SUPABASE_ENV_FILE} with Expo Supabase credentials for local app runs.\"\necho \"If Expo is already running, restart it to pick up updated EXPO_PUBLIC_* variables.\"\necho \"Next: run ./scripts/up.sh to sync Kubernetes secrets and apply infrastructure.\"\n`;\n}\n\nfunction getDefaultAppImage(namespace: string): string {\n return `ankh/${namespace}:dev`;\n}\n\nfunction resolveSupabaseLocalPorts(namespace: string): SupabaseLocalPorts {\n const base =\n SUPABASE_LOCAL_PORT_BASE +\n resolveSupabaseLocalPortBucket(namespace) * SUPABASE_LOCAL_PORT_BUCKET_SIZE;\n\n return {\n base,\n shadow: base,\n api: base + 1,\n db: base + 2,\n studio: base + 3,\n inbucket: base + 4,\n analytics: base + 5,\n };\n}\n\nfunction resolveSupabaseLocalPortBucket(namespace: string): number {\n const rawBucket = hashProjectId(namespace) - hashProjectId(SUPABASE_LOCAL_PORT_REFERENCE_PROJECT);\n return (rawBucket + SUPABASE_LOCAL_PORT_BUCKET_COUNT) % SUPABASE_LOCAL_PORT_BUCKET_COUNT;\n}\n\nfunction hashProjectId(value: string): number {\n const source = value.trim().length > 0 ? value.trim() : 'app';\n let hash = 0;\n\n for (const char of source) {\n hash = (hash * 31 + char.charCodeAt(0)) % SUPABASE_LOCAL_PORT_BUCKET_COUNT;\n }\n\n return hash;\n}\n\nfunction getSqlTextArray(values: readonly string[]): string {\n return `array[${values.map((value) => `'${escapeSqlLiteral(value)}'`).join(', ')}]::text[]`;\n}\n\nfunction escapeSqlLiteral(value: string): string {\n return value.replaceAll(\"'\", \"''\");\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/adapters/minikube/base/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { GeneratedInfrastructureFile } from '../../../types';\nimport type { InfraManifestInput } from '../../../types';\nimport {\n MANAGED_PROFILE_COLUMNS,\n type ResolvedProfileModel,\n resolveSupabaseProfileModel,\n} from '../auth/supabase/profile';\n\ninterface SupabaseLocalPorts {\n base: number;\n shadow: number;\n api: number;\n db: number;\n studio: number;\n inbucket: number;\n analytics: number;\n}\n\ninterface AppInfraStorageMetadata {\n provider: 'supabase';\n bucketsCsv: string;\n defaultBucket: string;\n}\n\nconst SUPABASE_LOCAL_PORT_BASE = 55320;\nconst SUPABASE_LOCAL_PORT_BUCKET_SIZE = 10;\nconst SUPABASE_LOCAL_PORT_BUCKET_COUNT = 1000;\nconst SUPABASE_LOCAL_PORT_REFERENCE_PROJECT = 'my-app';\n\nexport function generateMinikubeBaseArtifacts(args: {\n manifest: InfraManifestInput;\n namespace: string;\n supabaseProjectId: string | null;\n extraResources: string[];\n extraEnvEntries: string[];\n}): GeneratedInfrastructureFile[] {\n const { manifest, namespace, supabaseProjectId, extraResources, extraEnvEntries } = args;\n\n const root = 'infra/minikube';\n const k8sRoot = `${root}/k8s`;\n const scriptsRoot = `${root}/scripts`;\n const appImageRoot = `${root}/app-image`;\n const defaultAppImage = getDefaultAppImage(namespace);\n const supabaseLocalPorts = resolveSupabaseLocalPorts(supabaseProjectId ?? namespace);\n\n const authScope = manifest.auth?.scope ?? 'none';\n const authProvider = manifest.auth?.provider ?? 'none';\n const authzEngine = manifest.auth?.authorization?.engine ?? 'none';\n const databaseProvider = manifest.database?.provider ?? 'none';\n const secretStoreProvider = manifest.secretStore?.provider ?? 'none';\n const storageMetadata = resolveAppInfraStorageMetadata(manifest);\n const profileModel = resolveSupabaseProfileModel(manifest);\n const monitoringEnabled = manifest.deployment?.monitoring === true;\n const domain = manifest.networking?.domain ?? '';\n const supabaseLocalEnabled =\n authProvider === 'supabase' ||\n databaseProvider === 'supabase' ||\n storageMetadata !== null ||\n secretStoreProvider === 'supabase-vault';\n\n return [\n {\n path: `${root}/README.md`,\n content: getReadmeMarkdown({\n namespace,\n defaultAppImage,\n monitoringEnabled,\n authProvider,\n authzEngine,\n extraResources,\n supabaseLocalPorts,\n supabaseProjectId,\n supabaseLocalEnabled,\n profileModel,\n }),\n },\n {\n path: `${root}/.env.example`,\n content: getEnvExample({\n namespace,\n domain,\n extraEnvEntries,\n defaultAppImage,\n supabaseLocalPorts,\n supabaseLocalEnabled,\n }),\n },\n {\n path: `${k8sRoot}/namespace.yaml`,\n content: getNamespaceManifest(namespace),\n },\n {\n path: `${k8sRoot}/app.configmap.yaml`,\n content: getAppConfigMap({\n namespace,\n authScope,\n authProvider,\n databaseProvider,\n secretStoreProvider,\n storageMetadata,\n monitoringEnabled,\n domain,\n }),\n },\n {\n path: `${k8sRoot}/app/deployment.yaml`,\n content: getAppDeploymentManifest({ namespace, defaultAppImage }),\n },\n {\n path: `${k8sRoot}/app/service.yaml`,\n content: getAppServiceManifest(namespace),\n },\n {\n path: `${k8sRoot}/kustomization.yaml`,\n content: getKustomizationManifest(extraResources),\n },\n {\n path: `${scriptsRoot}/up.sh`,\n content: getUpScript({\n defaultNamespace: namespace,\n defaultAppImage,\n supabaseLocalEnabled,\n }),\n executable: true,\n },\n {\n path: `${scriptsRoot}/build-app-image.sh`,\n content: getBuildAppImageScript({ defaultNamespace: namespace, defaultAppImage }),\n executable: true,\n },\n {\n path: `${scriptsRoot}/port-forward.sh`,\n content: getPortForwardScript(namespace),\n executable: true,\n },\n {\n path: `${scriptsRoot}/down.sh`,\n content: getDownScript(),\n executable: true,\n },\n {\n path: `${scriptsRoot}/status.sh`,\n content: getStatusScript({\n defaultNamespace: namespace,\n supabaseProjectId,\n supabaseLocalEnabled,\n profileModel,\n }),\n executable: true,\n },\n {\n path: `${scriptsRoot}/supabase-local-env.sh`,\n content: getSupabaseLocalEnvScript({\n supabaseProjectId,\n supabaseLocalPorts,\n profileModel,\n }),\n executable: true,\n },\n {\n path: `${appImageRoot}/Dockerfile`,\n content: getAppImageDockerfile(),\n },\n ];\n}\n\nfunction getReadmeMarkdown(args: {\n namespace: string;\n defaultAppImage: string;\n monitoringEnabled: boolean;\n authProvider: string;\n authzEngine: string;\n extraResources: string[];\n supabaseLocalPorts: SupabaseLocalPorts;\n supabaseProjectId: string | null;\n supabaseLocalEnabled: boolean;\n profileModel: ResolvedProfileModel;\n}): string {\n const {\n namespace,\n defaultAppImage,\n monitoringEnabled,\n authProvider,\n authzEngine,\n extraResources,\n supabaseLocalPorts,\n supabaseProjectId,\n supabaseLocalEnabled,\n profileModel,\n } = args;\n const resourceLines = [\n 'namespace.yaml',\n 'app.configmap.yaml',\n 'app/deployment.yaml',\n 'app/service.yaml',\n ...extraResources,\n ]\n .map((r) => `- \\`k8s/${r}\\``)\n .join('\\n');\n\n return `# Minikube Infra\n\nThis directory is generated from \\`ankh.config.json\\` (infra manifest).\n\n## Requirements\n\n- minikube\n- kubectl\n- docker (when using the default driver)\n- bun (for Expo web export in app image build flow)\n- python3 (for patching generated local Supabase port defaults and checking port availability)\n\n## Quick Start\n\n1. Copy env template:\n \\`\\`\\`bash\n cp .env.example .env\n \\`\\`\\`\n2. Optional: import local Supabase credentials from \\`supabase status\\` output:\n \\`\\`\\`bash\n ./scripts/supabase-local-env.sh\n \\`\\`\\`\n3. Apply baseline infra:\n \\`\\`\\`bash\n ./scripts/up.sh\n \\`\\`\\`\n4. Start app port-forward:\n \\`\\`\\`bash\n ./scripts/port-forward.sh\n \\`\\`\\`\n5. Check status:\n \\`\\`\\`bash\n ./scripts/status.sh\n \\`\\`\\`\n6. Tear down:\n \\`\\`\\`bash\n ./scripts/down.sh\n \\`\\`\\`\n\n## Generated Resources\n\n${resourceLines}\n\n## Generated Helpers\n\n- \\`scripts/build-app-image.sh\\`\n- \\`scripts/port-forward.sh\\`\n- \\`scripts/supabase-local-env.sh\\`\n- \\`app-image/Dockerfile\\`\n\n## Runtime Conventions\n\n- Namespace: \\`${namespace}\\`\n- Supabase local project identity: \\`${supabaseProjectId ?? 'unused'}\\`\n- Monitoring requested: \\`${monitoringEnabled ? 'true' : 'false'}\\`\n- Auth provider: \\`${authProvider}\\`\n- Authorization engine: \\`${authzEngine}\\`\n- Namespace source: \\`infra.networking.domain\\` first, otherwise project slug hint from CLI.\n\n## App Runtime\n\nDefault app runtime manifests are generated:\n\n- \\`k8s/app/deployment.yaml\\`\n- \\`k8s/app/service.yaml\\`\n\nDeployment env wiring includes:\n\n- \\`ConfigMap/app-infra-config\\`\n- \\`ConfigMap/app-runtime-auth-env\\` (optional)\n- \\`ConfigMap/app-runtime-storage-env\\` (optional)\n- \\`Secret/supabase-auth-secrets\\` (optional)\n\nIf \\`auth.provider\\` is \\`supabase\\`, use:\n\n- \\`k8s/auth/supabase/supabase-auth.secret.yaml\\`\n- \\`k8s/auth/supabase/app-runtime-auth.env.configmap.yaml\\`\n- \\`auth/supabase-runtime-wiring.md\\`\n\nThe app deployment should import runtime config sources in this order:\n\n\\`\\`\\`yaml\nenvFrom:\n - configMapRef:\n name: app-runtime-auth-env\n - configMapRef:\n name: app-runtime-storage-env\n - secretRef:\n name: supabase-auth-secrets\n\\`\\`\\`\n\nDefault runtime knobs in \\`.env\\`:\n\n- \\`APP_BUILD_ENABLED\\`\n- \\`APP_SOURCE_DIR\\`\n- \\`APP_WEB_EXPORT_DIR\\`\n- \\`APP_IMAGE\\`\n- \\`APP_IMAGE_SYNC_STRATEGY\\`\n- \\`APP_IMAGE_CLEANUP_ON_DOWN\\`\n- \\`APP_IMAGE_CLEANUP_MINIKUBE\\`\n- \\`APP_IMAGE_CLEANUP_DOCKER\\`\n- \\`AUTH_RUNTIME_MODE\\`\n- \\`APP_PORT_FORWARD_LOCAL_PORT\\`\n- \\`APP_PORT_FORWARD_REMOTE_PORT\\`\n- \\`APP_REPLICAS\\`\n- \\`APP_FORCE_ROLLOUT_RESTART\\`\n- \\`SUPABASE_PROJECT_DIR\\` (optional supabase CLI project path)\n- \\`SUPABASE_LOCAL_PORT_BASE\\` (generated default \\`${supabaseLocalPorts.base}\\`)\n- \\`SUPABASE_LOCAL_API_PORT\\` (generated default \\`${supabaseLocalPorts.api}\\`)\n- \\`SUPABASE_LOCAL_DB_PORT\\` (generated default \\`${supabaseLocalPorts.db}\\`)\n- \\`SUPABASE_LOCAL_STUDIO_PORT\\` (generated default \\`${supabaseLocalPorts.studio}\\`)\n- \\`SUPABASE_LOCAL_INBUCKET_PORT\\` (generated default \\`${supabaseLocalPorts.inbucket}\\`)\n- \\`SUPABASE_LOCAL_ANALYTICS_PORT\\` (generated default \\`${supabaseLocalPorts.analytics}\\`)\n- \\`SUPABASE_LOCAL_SHADOW_PORT\\` (generated default \\`${supabaseLocalPorts.shadow}\\`)\n- \\`APP_IMAGE_PULL_SECRET_NAME\\` + credentials (optional, private registry)\n- \\`SUPABASE_SECRET_SYNC_ENABLED\\`\n\nGenerated app Supabase local defaults:\n\n- Port base: \\`${supabaseLocalPorts.base}\\`\n- API: \\`http://127.0.0.1:${supabaseLocalPorts.api}\\`\n- DB: \\`127.0.0.1:${supabaseLocalPorts.db}\\`\n- Studio: \\`http://127.0.0.1:${supabaseLocalPorts.studio}\\`\n- Inbucket: \\`http://127.0.0.1:${supabaseLocalPorts.inbucket}\\`\n- Analytics: \\`http://127.0.0.1:${supabaseLocalPorts.analytics}\\`\n- Shadow DB: \\`127.0.0.1:${supabaseLocalPorts.shadow}\\`\n\nOverride with:\n\n- \\`SUPABASE_LOCAL_PORT_BASE\\`\n- \\`SUPABASE_LOCAL_API_PORT\\`\n- \\`SUPABASE_LOCAL_DB_PORT\\`\n- \\`SUPABASE_LOCAL_STUDIO_PORT\\`\n- \\`SUPABASE_LOCAL_INBUCKET_PORT\\`\n- \\`SUPABASE_LOCAL_ANALYTICS_PORT\\`\n- \\`SUPABASE_LOCAL_SHADOW_PORT\\`\n\nBuild flow:\n\n- \\`./scripts/up.sh\\` calls \\`./scripts/build-app-image.sh\\` when \\`APP_BUILD_ENABLED=true\\`.\n- Supabase local project root: \\`infra/minikube\\`${supabaseLocalEnabled ? '' : ' (unused unless Supabase-backed auth/database/storage is enabled)'}.\n- Supabase local project identity: \\`${supabaseProjectId ?? 'unused'}\\`; existing local configs with another \\`project_id\\` must be destroyed and recreated manually.\n- Immutable migrations live in \\`supabase/migrations/\\` and are applied with \\`supabase migration up --local\\`.\n${profileModel.enabled ? '- Generated profile desired-state reconciliation lives in `supabase/generated/auth_profiles.sql` and records checksum state in `ankhorage_internal.generated_schema_state`.\\n' : ''}- \\`./scripts/supabase-local-env.sh\\` can populate Supabase keys in \\`.env\\` from local \\`supabase status -o env\\`.\n- \\`./scripts/supabase-local-env.sh\\` writes \\`EXPO_PUBLIC_SUPABASE_URL\\` + \\`EXPO_PUBLIC_SUPABASE_ANON_KEY\\` into \\`$APP_SOURCE_DIR/.env.local\\` (e.g. \\`apps/card/.env.local\\`) for local app runs.\n- With \\`AUTH_RUNTIME_MODE=local\\` (default), \\`up.sh\\` runs \\`supabase-local-env.sh\\` before app/Kubernetes startup when Supabase-backed local services are enabled.\n- \\`supabase-local-env.sh\\` checks configured host ports before starting a missing local Supabase stack.\n- \\`build-app-image.sh\\` runs \\`bunx expo export --platform web\\` from app source.\n- \\`app-image/Dockerfile\\` builds an nginx image from exported web artifacts.\n- \\`build-app-image.sh\\` labels the generated Docker image with Ankhorage metadata for diagnostics.\n- \\`up.sh\\` syncs runtime image by strategy:\n - \\`APP_IMAGE_SYNC_STRATEGY=docker-load\\` (default): load prebuilt local Docker image into Minikube.\n - \\`APP_IMAGE_SYNC_STRATEGY=minikube-build\\`: build directly into Minikube image store.\n - \\`APP_IMAGE_SYNC_STRATEGY=none\\`: skip local image sync.\n- \\`up.sh\\` applies image/replica changes and (by default) restarts \\`deployment/app-runtime\\`.\n- If Supabase keys are provided, \\`up.sh\\` syncs \\`Secret/supabase-auth-secrets\\` from env.\n\nDefault generated app image tag: \\`${defaultAppImage}\\`.\n\n## Notes\n\n- Auth/database/authorization resources are generated in adapter modules.\n- Cerbos route/screen policies derive from app navigator + authFlow intent when available.\n- Re-generate infra by saving the manifest or calling the bridge infra endpoint.\n`;\n}\n\nfunction getEnvExample(args: {\n namespace: string;\n domain: string;\n extraEnvEntries: string[];\n defaultAppImage: string;\n supabaseLocalPorts: SupabaseLocalPorts;\n supabaseLocalEnabled: boolean;\n}): string {\n const { namespace, domain, extraEnvEntries, defaultAppImage, supabaseLocalPorts } = args;\n\n const baseEntries = [\n '# Minikube runtime configuration',\n `ANKH_NAMESPACE=${namespace}`,\n 'MINIKUBE_PROFILE=minikube',\n 'MINIKUBE_DRIVER=docker',\n 'APP_BUILD_ENABLED=true',\n 'APP_SOURCE_DIR=',\n 'APP_WEB_EXPORT_DIR=.ankh/web-export',\n '# Canonical Supabase project root. Leave empty to use infra/minikube.',\n 'SUPABASE_PROJECT_DIR=',\n `SUPABASE_LOCAL_PORT_BASE=${supabaseLocalPorts.base}`,\n `SUPABASE_LOCAL_SHADOW_PORT=${supabaseLocalPorts.shadow}`,\n `SUPABASE_LOCAL_API_PORT=${supabaseLocalPorts.api}`,\n `SUPABASE_LOCAL_DB_PORT=${supabaseLocalPorts.db}`,\n `SUPABASE_LOCAL_STUDIO_PORT=${supabaseLocalPorts.studio}`,\n `SUPABASE_LOCAL_INBUCKET_PORT=${supabaseLocalPorts.inbucket}`,\n `SUPABASE_LOCAL_ANALYTICS_PORT=${supabaseLocalPorts.analytics}`,\n `APP_IMAGE=${defaultAppImage}`,\n 'APP_IMAGE_SYNC_STRATEGY=docker-load',\n 'APP_IMAGE_CLEANUP_ON_DOWN=true',\n 'APP_IMAGE_CLEANUP_MINIKUBE=true',\n 'APP_IMAGE_CLEANUP_DOCKER=true',\n 'AUTH_RUNTIME_MODE=local',\n 'APP_PORT_FORWARD_LOCAL_PORT=18080',\n 'APP_PORT_FORWARD_REMOTE_PORT=80',\n 'APP_REPLICAS=1',\n 'APP_FORCE_ROLLOUT_RESTART=true',\n '',\n '# Optional private registry auth for APP_IMAGE',\n 'APP_IMAGE_PULL_SECRET_NAME=',\n 'APP_IMAGE_PULL_SECRET_SERVER=ghcr.io',\n 'APP_IMAGE_PULL_SECRET_USERNAME=',\n 'APP_IMAGE_PULL_SECRET_PASSWORD=',\n 'APP_IMAGE_PULL_SECRET_EMAIL=',\n '',\n '# Optional app networking domain',\n `APP_DOMAIN=${domain}`,\n '',\n '# Adapter-provided runtime keys',\n ];\n\n const merged = [...baseEntries, ...extraEnvEntries];\n const unique: string[] = [];\n for (const entry of merged) {\n if (!unique.includes(entry)) unique.push(entry);\n }\n\n return `${unique.join('\\n')}\\n`;\n}\n\nfunction getNamespaceManifest(namespace: string): string {\n return `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${namespace}\n labels:\n app.kubernetes.io/part-of: ankhorage\n`;\n}\n\nfunction getAppConfigMap(args: {\n namespace: string;\n authScope: string;\n authProvider: string;\n databaseProvider: string;\n secretStoreProvider: string;\n storageMetadata: AppInfraStorageMetadata | null;\n monitoringEnabled: boolean;\n domain: string;\n}): string {\n const {\n namespace,\n authScope,\n authProvider,\n databaseProvider,\n secretStoreProvider,\n storageMetadata,\n monitoringEnabled,\n domain,\n } = args;\n\n const storageLines = storageMetadata\n ? ` STORAGE_PROVIDER: \"${storageMetadata.provider}\"\n STORAGE_BUCKETS: \"${escapeYamlDoubleQuoted(storageMetadata.bucketsCsv)}\"\n STORAGE_DEFAULT_BUCKET: \"${escapeYamlDoubleQuoted(storageMetadata.defaultBucket)}\"\n`\n : '';\n\n return `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app-infra-config\n namespace: ${namespace}\ndata:\n DEPLOYMENT_TARGET: \"minikube\"\n MONITORING_ENABLED: \"${monitoringEnabled ? 'true' : 'false'}\"\n AUTH_SCOPE: \"${authScope}\"\n AUTH_PROVIDER: \"${authProvider}\"\n DATABASE_PROVIDER: \"${databaseProvider}\"\n SECRET_STORE_PROVIDER: \"${secretStoreProvider}\"\n${storageLines} NETWORK_DOMAIN: \"${domain}\"\n`;\n}\n\nfunction resolveAppInfraStorageMetadata(\n manifest: InfraManifestInput,\n): AppInfraStorageMetadata | null {\n const spec = manifest.storage;\n if (!spec) return null;\n\n const buckets = normalizeBuckets(spec.buckets);\n if (buckets.length === 0) return null;\n const [defaultBucket] = buckets;\n if (!defaultBucket) return null;\n\n if (spec.provider === 'supabase') {\n return {\n provider: 'supabase',\n bucketsCsv: buckets.join(','),\n defaultBucket,\n };\n }\n\n if (spec.provider === 'auto') {\n const shouldResolveToSupabase =\n manifest.database?.provider === 'supabase' || manifest.auth?.provider === 'supabase';\n\n if (!shouldResolveToSupabase) return null;\n\n return {\n provider: 'supabase',\n bucketsCsv: buckets.join(','),\n defaultBucket,\n };\n }\n\n return null;\n}\n\nfunction normalizeBuckets(rawBuckets: readonly string[]): string[] {\n const normalized: string[] = [];\n const seen = new Set<string>();\n\n for (const raw of rawBuckets) {\n const bucket = raw.trim();\n if (!bucket) continue;\n if (seen.has(bucket)) continue;\n seen.add(bucket);\n normalized.push(bucket);\n }\n\n return normalized;\n}\n\nfunction escapeYamlDoubleQuoted(value: string): string {\n return value.replaceAll('\\\\', '\\\\\\\\').replaceAll('\"', '\\\\\"');\n}\n\nfunction getKustomizationManifest(extraResources: string[]): string {\n const resources = [\n 'namespace.yaml',\n 'app.configmap.yaml',\n 'app/deployment.yaml',\n 'app/service.yaml',\n ...extraResources,\n ];\n const resourceLines = resources.map((r) => ` - ${r}`).join('\\n');\n\n return `apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n${resourceLines}\n`;\n}\n\nfunction getAppDeploymentManifest(args: { namespace: string; defaultAppImage: string }): string {\n const { namespace, defaultAppImage } = args;\n\n return `apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app-runtime\n namespace: ${namespace}\n labels:\n app.kubernetes.io/name: app-runtime\nspec:\n replicas: 1\n selector:\n matchLabels:\n app.kubernetes.io/name: app-runtime\n template:\n metadata:\n labels:\n app.kubernetes.io/name: app-runtime\n spec:\n containers:\n - name: app\n image: ${defaultAppImage}\n imagePullPolicy: IfNotPresent\n ports:\n - containerPort: 8080\n name: http\n envFrom:\n - configMapRef:\n name: app-infra-config\n - configMapRef:\n name: app-runtime-auth-env\n optional: true\n - configMapRef:\n name: app-runtime-storage-env\n optional: true\n - secretRef:\n name: supabase-auth-secrets\n optional: true\n readinessProbe:\n httpGet:\n path: /\n port: http\n initialDelaySeconds: 5\n periodSeconds: 10\n livenessProbe:\n httpGet:\n path: /\n port: http\n initialDelaySeconds: 15\n periodSeconds: 20\n`;\n}\n\nfunction getAppServiceManifest(namespace: string): string {\n return `apiVersion: v1\nkind: Service\nmetadata:\n name: app-runtime\n namespace: ${namespace}\n labels:\n app.kubernetes.io/name: app-runtime\nspec:\n type: ClusterIP\n selector:\n app.kubernetes.io/name: app-runtime\n ports:\n - name: http\n port: 80\n targetPort: http\n`;\n}\n\nfunction getUpScript(args: {\n defaultNamespace: string;\n defaultAppImage: string;\n supabaseLocalEnabled: boolean;\n}): string {\n const { defaultNamespace, defaultAppImage, supabaseLocalEnabled } = args;\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\nK8S_DIR=\"\\${ROOT_DIR}/k8s\"\nBUILD_SCRIPT=\"\\${SCRIPT_DIR}/build-app-image.sh\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nPROFILE=\"\\${MINIKUBE_PROFILE:-minikube}\"\nDRIVER=\"\\${MINIKUBE_DRIVER:-docker}\"\nNAMESPACE=\"\\${ANKH_NAMESPACE:-${defaultNamespace}}\"\nAPP_BUILD_ENABLED=\"\\${APP_BUILD_ENABLED:-true}\"\nAPP_SOURCE_DIR=\"\\${APP_SOURCE_DIR:-$(cd \"\\${ROOT_DIR}/../..\" && pwd)}\"\nAPP_WEB_EXPORT_DIR=\"\\${APP_WEB_EXPORT_DIR:-.ankh/web-export}\"\nAPP_IMAGE=\"\\${APP_IMAGE:-${defaultAppImage}}\"\nAPP_IMAGE_SYNC_STRATEGY=\"\\${APP_IMAGE_SYNC_STRATEGY:-docker-load}\"\nAPP_REPLICAS=\"\\${APP_REPLICAS:-1}\"\nAPP_FORCE_ROLLOUT_RESTART=\"\\${APP_FORCE_ROLLOUT_RESTART:-true}\"\nAPP_IMAGE_PULL_SECRET_NAME=\"\\${APP_IMAGE_PULL_SECRET_NAME:-}\"\nAPP_IMAGE_PULL_SECRET_SERVER=\"\\${APP_IMAGE_PULL_SECRET_SERVER:-ghcr.io}\"\nAPP_IMAGE_PULL_SECRET_USERNAME=\"\\${APP_IMAGE_PULL_SECRET_USERNAME:-}\"\nAPP_IMAGE_PULL_SECRET_PASSWORD=\"\\${APP_IMAGE_PULL_SECRET_PASSWORD:-}\"\nAPP_IMAGE_PULL_SECRET_EMAIL=\"\\${APP_IMAGE_PULL_SECRET_EMAIL:-}\"\nAUTH_RUNTIME_MODE=\"\\${AUTH_RUNTIME_MODE:-local}\"\nSUPABASE_LOCAL_ENABLED=\"\\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}\"\nSUPABASE_SECRET_SYNC_ENABLED=\"\\${SUPABASE_SECRET_SYNC_ENABLED:-true}\"\nSUPABASE_LOCAL_ENV_SCRIPT=\"\\${SCRIPT_DIR}/supabase-local-env.sh\"\nSUPABASE_URL=\"\\${SUPABASE_URL:-}\"\nSUPABASE_ANON_KEY=\"\\${SUPABASE_ANON_KEY:-}\"\nSUPABASE_SERVICE_ROLE_KEY=\"\\${SUPABASE_SERVICE_ROLE_KEY:-}\"\nSUPABASE_JWT_SECRET=\"\\${SUPABASE_JWT_SECRET:-}\"\nSUPABASE_PUBLIC_URL=\"\\${EXPO_PUBLIC_SUPABASE_URL:-\\${SUPABASE_URL}}\"\nSUPABASE_PUBLIC_ANON_KEY=\"\\${EXPO_PUBLIC_SUPABASE_ANON_KEY:-\\${SUPABASE_ANON_KEY}}\"\n\nrefresh_supabase_env() {\n SUPABASE_URL=\"\\${SUPABASE_URL:-}\"\n SUPABASE_ANON_KEY=\"\\${SUPABASE_ANON_KEY:-}\"\n SUPABASE_SERVICE_ROLE_KEY=\"\\${SUPABASE_SERVICE_ROLE_KEY:-}\"\n SUPABASE_JWT_SECRET=\"\\${SUPABASE_JWT_SECRET:-}\"\n SUPABASE_PUBLIC_URL=\"\\${EXPO_PUBLIC_SUPABASE_URL:-\\${SUPABASE_URL}}\"\n SUPABASE_PUBLIC_ANON_KEY=\"\\${EXPO_PUBLIC_SUPABASE_ANON_KEY:-\\${SUPABASE_ANON_KEY}}\"\n}\n\nhas_required_supabase_env() {\n [[ -n \"\\${SUPABASE_URL}\" && -n \"\\${SUPABASE_ANON_KEY}\" && -n \"\\${SUPABASE_SERVICE_ROLE_KEY}\" && -n \"\\${SUPABASE_JWT_SECRET}\" ]]\n}\n\nrefresh_supabase_env\n\nif [[ \"\\${AUTH_RUNTIME_MODE}\" != \"local\" && \"\\${AUTH_RUNTIME_MODE}\" != \"remote\" ]]; then\n echo \"Unsupported AUTH_RUNTIME_MODE=\\${AUTH_RUNTIME_MODE}. Use local or remote.\"\n exit 1\nfi\n\nif ! command -v minikube >/dev/null 2>&1; then\n echo \"minikube is required but not installed.\"\n exit 1\nfi\n\nif ! command -v kubectl >/dev/null 2>&1; then\n echo \"kubectl is required but not installed.\"\n exit 1\nfi\n\nHOST_STATUS=\"$(minikube -p \"\\${PROFILE}\" status --format='{{.Host}}' 2>/dev/null || true)\"\nif [[ \"\\${HOST_STATUS}\" != \"Running\" ]]; then\n minikube start -p \"\\${PROFILE}\" --driver=\"\\${DRIVER}\"\nfi\n\nif [[ \"\\${AUTH_RUNTIME_MODE}\" == \"local\" && \"\\${SUPABASE_LOCAL_ENABLED}\" == \"true\" ]]; then\n if [[ ! -x \"\\${SUPABASE_LOCAL_ENV_SCRIPT}\" ]]; then\n echo \"Supabase local bootstrap failed: missing executable \\${SUPABASE_LOCAL_ENV_SCRIPT}.\"\n exit 1\n else\n echo \"Running local Supabase bootstrap, immutable migrations, generated reconciliation, and schema verification.\"\n if ! \"\\${SUPABASE_LOCAL_ENV_SCRIPT}\"; then\n echo \"Supabase local bootstrap failed.\"\n exit 1\n fi\n if [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\n fi\n refresh_supabase_env\n fi\nfi\n\nif [[ \"\\${APP_BUILD_ENABLED}\" == \"true\" ]]; then\n if [[ ! -x \"\\${BUILD_SCRIPT}\" ]]; then\n echo \"Missing build helper script: \\${BUILD_SCRIPT}\"\n exit 1\n fi\n\n \"\\${BUILD_SCRIPT}\"\nfi\n\nif [[ \"\\${APP_WEB_EXPORT_DIR}\" = /* ]]; then\n EXPORT_DIR=\"\\${APP_WEB_EXPORT_DIR}\"\nelse\n EXPORT_DIR=\"\\${APP_SOURCE_DIR}/\\${APP_WEB_EXPORT_DIR}\"\nfi\n\ndocker_load_image_to_minikube() {\n if command -v docker >/dev/null 2>&1 && docker image inspect \"\\${APP_IMAGE}\" >/dev/null 2>&1; then\n minikube -p \"\\${PROFILE}\" image load --daemon=true --overwrite=true \"\\${APP_IMAGE}\" >/dev/null\n return 0\n fi\n\n return 1\n}\n\ncase \"\\${APP_IMAGE_SYNC_STRATEGY}\" in\n minikube-build)\n if [[ ! -f \"\\${EXPORT_DIR}/index.html\" ]]; then\n echo \"Expected web export output not found at \\${EXPORT_DIR}/index.html\"\n echo \"Run build-app-image.sh first or switch APP_IMAGE_SYNC_STRATEGY=docker-load.\"\n exit 1\n fi\n\n if minikube -p \"\\${PROFILE}\" image build -t \"\\${APP_IMAGE}\" -f \"\\${ROOT_DIR}/app-image/Dockerfile\" \"\\${EXPORT_DIR}\" >/dev/null; then\n if ! minikube -p \"\\${PROFILE}\" image ls 2>/dev/null | grep -Fq \"\\${APP_IMAGE}\"; then\n echo \"minikube image build did not register \\${APP_IMAGE}; falling back to docker-load.\"\n if ! docker_load_image_to_minikube; then\n echo \"Fallback docker-load failed: local Docker image \\${APP_IMAGE} not found.\"\n exit 1\n fi\n fi\n else\n echo \"minikube image build failed for \\${APP_IMAGE}; falling back to docker-load.\"\n if ! docker_load_image_to_minikube; then\n echo \"Fallback docker-load failed: local Docker image \\${APP_IMAGE} not found.\"\n exit 1\n fi\n fi\n ;;\n docker-load)\n if ! docker_load_image_to_minikube; then\n echo \"Skipping docker-load image sync; local Docker image \\${APP_IMAGE} not found.\"\n fi\n ;;\n none)\n echo \"Skipping local image sync (APP_IMAGE_SYNC_STRATEGY=none).\"\n ;;\n *)\n echo \"Unsupported APP_IMAGE_SYNC_STRATEGY=\\${APP_IMAGE_SYNC_STRATEGY}. Use minikube-build, docker-load, or none.\"\n exit 1\n ;;\nesac\n\nkubectl apply -k \"\\${K8S_DIR}\"\n\nif [[ \"\\${SUPABASE_SECRET_SYNC_ENABLED}\" == \"true\" ]]; then\n if ! has_required_supabase_env && [[ \"\\${AUTH_RUNTIME_MODE}\" == \"local\" && \"\\${SUPABASE_LOCAL_ENABLED}\" == \"true\" ]]; then\n if [[ ! -x \"\\${SUPABASE_LOCAL_ENV_SCRIPT}\" ]]; then\n echo \"Supabase local bootstrap failed: missing executable \\${SUPABASE_LOCAL_ENV_SCRIPT}.\"\n exit 1\n else\n echo \"Supabase keys missing; running \\${SUPABASE_LOCAL_ENV_SCRIPT}.\"\n if ! \"\\${SUPABASE_LOCAL_ENV_SCRIPT}\"; then\n echo \"Supabase local bootstrap failed.\"\n exit 1\n fi\n if [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\n fi\n refresh_supabase_env\n fi\n fi\n\n if ! has_required_supabase_env; then\n if [[ \"\\${AUTH_RUNTIME_MODE}\" == \"local\" ]]; then\n echo \"Supabase secret sync skipped: local credentials still missing after bootstrap.\"\n echo \"Run ./scripts/supabase-local-env.sh and retry.\"\n else\n echo \"Supabase secret sync skipped: set SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, and SUPABASE_JWT_SECRET in .env.\"\n fi\n else\n TMP_SUPABASE_ENV_FILE=\"$(mktemp)\"\n trap 'rm -f \"\\${TMP_SUPABASE_ENV_FILE}\"' EXIT\n\n cat > \"\\${TMP_SUPABASE_ENV_FILE}\" <<EOF\nSUPABASE_URL=\\${SUPABASE_URL}\nSUPABASE_ANON_KEY=\\${SUPABASE_ANON_KEY}\nSUPABASE_SERVICE_ROLE_KEY=\\${SUPABASE_SERVICE_ROLE_KEY}\nSUPABASE_JWT_SECRET=\\${SUPABASE_JWT_SECRET}\nEXPO_PUBLIC_SUPABASE_URL=\\${SUPABASE_PUBLIC_URL}\nEXPO_PUBLIC_SUPABASE_ANON_KEY=\\${SUPABASE_PUBLIC_ANON_KEY}\nEOF\n\n kubectl -n \"\\${NAMESPACE}\" create secret generic supabase-auth-secrets --from-env-file=\"\\${TMP_SUPABASE_ENV_FILE}\" --dry-run=client -o yaml | kubectl -n \"\\${NAMESPACE}\" apply -f -\n\n rm -f \"\\${TMP_SUPABASE_ENV_FILE}\"\n trap - EXIT\n echo \"Synchronized supabase-auth-secrets from environment values.\"\n fi\nfi\n\nif [[ -n \"\\${APP_IMAGE_PULL_SECRET_NAME}\" ]]; then\n if [[ -z \"\\${APP_IMAGE_PULL_SECRET_USERNAME}\" || -z \"\\${APP_IMAGE_PULL_SECRET_PASSWORD}\" ]]; then\n echo \"APP_IMAGE_PULL_SECRET_NAME is set, but username/password are missing.\"\n exit 1\n fi\n\n SECRET_EMAIL_ARG=()\n if [[ -n \"\\${APP_IMAGE_PULL_SECRET_EMAIL}\" ]]; then\n SECRET_EMAIL_ARG=(--docker-email=\"\\${APP_IMAGE_PULL_SECRET_EMAIL}\")\n fi\n\n kubectl -n \"\\${NAMESPACE}\" create secret docker-registry \"\\${APP_IMAGE_PULL_SECRET_NAME}\" --docker-server=\"\\${APP_IMAGE_PULL_SECRET_SERVER}\" --docker-username=\"\\${APP_IMAGE_PULL_SECRET_USERNAME}\" --docker-password=\"\\${APP_IMAGE_PULL_SECRET_PASSWORD}\" \"\\${SECRET_EMAIL_ARG[@]}\" --dry-run=client -o yaml | kubectl -n \"\\${NAMESPACE}\" apply -f -\n\n PULL_SECRET_PATCH=\"$(cat <<EOF\n{\"spec\":{\"template\":{\"spec\":{\"imagePullSecrets\":[{\"name\":\"\\${APP_IMAGE_PULL_SECRET_NAME}\"}]}}}}\nEOF\n)\"\n kubectl -n \"\\${NAMESPACE}\" patch deployment app-runtime --type=merge --patch \"\\${PULL_SECRET_PATCH}\" >/dev/null\nelse\n kubectl -n \"\\${NAMESPACE}\" patch deployment app-runtime --type=json --patch='[{\"op\":\"remove\",\"path\":\"/spec/template/spec/imagePullSecrets\"}]' >/dev/null 2>&1 || true\nfi\n\nkubectl -n \"\\${NAMESPACE}\" set image deployment/app-runtime app=\"\\${APP_IMAGE}\" >/dev/null\nkubectl -n \"\\${NAMESPACE}\" scale deployment/app-runtime --replicas=\"\\${APP_REPLICAS}\" >/dev/null\n\nif [[ \"\\${APP_FORCE_ROLLOUT_RESTART}\" == \"true\" ]]; then\n kubectl -n \"\\${NAMESPACE}\" rollout restart deployment/app-runtime >/dev/null\nfi\n\nkubectl -n \"\\${NAMESPACE}\" rollout status deployment/app-runtime --timeout=180s >/dev/null\necho \"Minikube infrastructure applied.\"\n`;\n}\n\nfunction getBuildAppImageScript(args: {\n defaultNamespace: string;\n defaultAppImage: string;\n}): string {\n const { defaultNamespace, defaultAppImage } = args;\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\nDOCKERFILE_PATH=\"\\${ROOT_DIR}/app-image/Dockerfile\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nNAMESPACE=\"\\${ANKH_NAMESPACE:-${defaultNamespace}}\"\nAPP_BUILD_ENABLED=\"\\${APP_BUILD_ENABLED:-true}\"\nAPP_SOURCE_DIR=\"\\${APP_SOURCE_DIR:-$(cd \"\\${ROOT_DIR}/../..\" && pwd)}\"\nAPP_WEB_EXPORT_DIR=\"\\${APP_WEB_EXPORT_DIR:-.ankh/web-export}\"\nAPP_IMAGE=\"\\${APP_IMAGE:-${defaultAppImage}}\"\n\nif [[ \"\\${APP_BUILD_ENABLED}\" != \"true\" ]]; then\n echo \"Skipping app image build (APP_BUILD_ENABLED=\\${APP_BUILD_ENABLED}).\"\n exit 0\nfi\n\nif ! command -v docker >/dev/null 2>&1; then\n echo \"docker is required to build app image.\"\n exit 1\nfi\n\nif ! command -v bunx >/dev/null 2>&1; then\n echo \"bunx is required to export the app for container image build.\"\n exit 1\nfi\n\nif [[ ! -f \"\\${APP_SOURCE_DIR}/package.json\" ]]; then\n echo \"APP_SOURCE_DIR does not look like an app root: \\${APP_SOURCE_DIR}\"\n exit 1\nfi\n\nif [[ ! -f \"\\${DOCKERFILE_PATH}\" ]]; then\n echo \"Dockerfile for app image build is missing: \\${DOCKERFILE_PATH}\"\n exit 1\nfi\n\n(\n cd \"\\${APP_SOURCE_DIR}\"\n bunx expo export --platform web --output-dir \"\\${APP_WEB_EXPORT_DIR}\"\n)\n\nif [[ \"\\${APP_WEB_EXPORT_DIR}\" = /* ]]; then\n EXPORT_DIR=\"\\${APP_WEB_EXPORT_DIR}\"\nelse\n EXPORT_DIR=\"\\${APP_SOURCE_DIR}/\\${APP_WEB_EXPORT_DIR}\"\nfi\n\nif [[ ! -f \"\\${EXPORT_DIR}/index.html\" ]]; then\n echo \"Expected web export output not found at \\${EXPORT_DIR}/index.html\"\n exit 1\nfi\n\ndocker build -t \"\\${APP_IMAGE}\" \\\n --label \"ankhorage.kind=generated-app\" \\\n --label \"ankhorage.namespace=\\${NAMESPACE}\" \\\n --label \"ankhorage.image=\\${APP_IMAGE}\" \\\n -f \"\\${DOCKERFILE_PATH}\" \\\n \"\\${EXPORT_DIR}\"\necho \"Built app image: \\${APP_IMAGE} (namespace: \\${NAMESPACE})\"\n`;\n}\n\nfunction getAppImageDockerfile(): string {\n return `FROM nginx:1.27-alpine\n\nRUN cat <<'EOF' > /etc/nginx/conf.d/default.conf\nserver {\n listen 8080;\n server_name _;\n root /usr/share/nginx/html;\n index index.html;\n\n location / {\n try_files $uri $uri/ /index.html;\n }\n}\nEOF\n\nCOPY . /usr/share/nginx/html\n\nEXPOSE 8080\n\nCMD [\"nginx\", \"-g\", \"daemon off;\"]\n`;\n}\n\nfunction getPortForwardScript(defaultNamespace: string): string {\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nNAMESPACE=\"\\${ANKH_NAMESPACE:-${defaultNamespace}}\"\nLOCAL_PORT=\"\\${APP_PORT_FORWARD_LOCAL_PORT:-18080}\"\nREMOTE_PORT=\"\\${APP_PORT_FORWARD_REMOTE_PORT:-80}\"\n\nif ! command -v kubectl >/dev/null 2>&1; then\n echo \"kubectl is required but not installed.\"\n exit 1\nfi\n\nif ! kubectl get namespace \"\\${NAMESPACE}\" >/dev/null 2>&1; then\n echo \"Namespace '\\${NAMESPACE}' was not found.\"\n echo \"Run ./scripts/up.sh to create infrastructure first.\"\n exit 1\nfi\n\nif ! kubectl -n \"\\${NAMESPACE}\" get service app-runtime >/dev/null 2>&1; then\n echo \"Service 'app-runtime' was not found in namespace '\\${NAMESPACE}'.\"\n echo \"Run ./scripts/up.sh to apply runtime resources.\"\n exit 1\nfi\n\necho \"Forwarding app-runtime from namespace '\\${NAMESPACE}' to http://127.0.0.1:\\${LOCAL_PORT}\"\necho \"Press Ctrl+C to stop.\"\nkubectl -n \"\\${NAMESPACE}\" port-forward service/app-runtime \"\\${LOCAL_PORT}:\\${REMOTE_PORT}\"\n`;\n}\n\nfunction getDownScript(): string {\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\nK8S_DIR=\"\\${ROOT_DIR}/k8s\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nkubectl delete -k \"\\${K8S_DIR}\" --ignore-not-found\necho \"Minikube infrastructure removed.\"\n`;\n}\n\nfunction getSupabaseProfileChecksumSql(profileModel: ResolvedProfileModel): string {\n return `do $$\ndeclare\n actual_hash text;\n actual_table_name text;\nbegin\n if to_regclass('ankhorage_internal.generated_schema_state') is null then\n raise exception 'missing ankhorage_internal.generated_schema_state';\n end if;\n\n select content_hash, table_name\n into actual_hash, actual_table_name\n from ankhorage_internal.generated_schema_state\n where artifact_key = 'auth.profile';\n\n if actual_hash is null then\n raise exception 'missing generated schema state for auth.profile';\n end if;\n\n if actual_table_name <> '${escapeSqlLiteral(profileModel.table)}' then\n raise exception 'generated schema state table mismatch for auth.profile: expected %, found %', '${escapeSqlLiteral(profileModel.table)}', actual_table_name;\n end if;\n\n if actual_hash <> '${escapeSqlLiteral(profileModel.desiredStateHash)}' then\n raise exception 'stale generated schema state for auth.profile: expected %, found %', '${escapeSqlLiteral(profileModel.desiredStateHash)}', actual_hash;\n end if;\nend;\n$$;`;\n}\n\nfunction getSupabaseProfileDisabledGuardSql(): string {\n return `do $$\ndeclare\n has_profile_state boolean := false;\nbegin\n if to_regclass('ankhorage_internal.generated_schema_state') is not null then\n execute\n 'select exists (\n select 1\n from ankhorage_internal.generated_schema_state\n where artifact_key = $1\n )'\n into has_profile_state\n using 'auth.profile';\n end if;\n\n if has_profile_state then\n raise exception 'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration';\n end if;\nend;\n$$;`;\n}\n\nfunction getSupabaseProfileVerificationSql(profileModel: ResolvedProfileModel): string {\n const configuredColumns = profileModel.columns.map((column) => column.column);\n const configuredArray = getSqlTextArray(configuredColumns);\n const managedArray = getSqlTextArray(MANAGED_PROFILE_COLUMNS);\n const updateGrantArray = getSqlTextArray(configuredColumns);\n const protectedUpdateColumnsArray = getSqlTextArray(['id', 'created_at', 'updated_at', 'role']);\n const table = escapeSqlLiteral(profileModel.table);\n const selectPolicy = escapeSqlLiteral(`${profileModel.table}_select_own`);\n const updatePolicy = escapeSqlLiteral(`${profileModel.table}_update_own`);\n const triggerName = escapeSqlLiteral(`on_auth_user_created_${profileModel.table}`);\n const functionName = escapeSqlLiteral(`handle_new_${profileModel.table}_user`);\n const triggerCheck =\n profileModel.createStrategy === 'trigger'\n ? ` if not exists (\n select 1\n from pg_trigger t\n join pg_class rel on rel.oid = t.tgrelid\n join pg_namespace rel_ns on rel_ns.oid = rel.relnamespace\n join pg_proc fn on fn.oid = t.tgfoid\n join pg_namespace fn_ns on fn_ns.oid = fn.pronamespace\n where rel_ns.nspname = 'auth'\n and rel.relname = 'users'\n and t.tgname = '${triggerName}'\n and not t.tgisinternal\n and t.tgenabled = 'O'\n and (t.tgtype::integer & 1) = 1\n and (t.tgtype::integer & 4) = 4\n and (t.tgtype::integer & (2 | 8 | 16 | 32)) = 0\n and fn_ns.nspname = 'public'\n and fn.proname = '${functionName}'\n ) then\n raise exception 'missing generated new-user profile trigger';\n end if;`\n : ` if exists (\n select 1\n from pg_trigger t\n join pg_class rel on rel.oid = t.tgrelid\n join pg_namespace rel_ns on rel_ns.oid = rel.relnamespace\n where rel_ns.nspname = 'auth'\n and rel.relname = 'users'\n and t.tgname = '${triggerName}'\n and not t.tgisinternal\n ) then\n raise exception 'generated new-user profile trigger exists but createStrategy is not trigger';\n end if;`;\n\n return `do $$\ndeclare\n profile_table constant text := '${table}';\n configured_columns constant text[] := ${configuredArray};\n managed_columns constant text[] := ${managedArray};\n expected_column text;\nbegin\n if to_regclass('auth.users') is null then\n raise exception 'missing auth.users';\n end if;\n\n if to_regclass('public.users') is not null then\n raise exception 'reserved conflicting identity table public.users exists';\n end if;\n\n if to_regclass(format('public.%I', profile_table)) is null then\n raise exception 'missing configured profile table public.%', profile_table;\n end if;\n\n if not exists (\n select 1\n from pg_constraint c\n where c.conrelid = format('public.%I', profile_table)::regclass\n and c.contype = 'p'\n and c.conkey = array[\n (\n select a.attnum\n from pg_attribute a\n where a.attrelid = format('public.%I', profile_table)::regclass\n and a.attname = 'id'\n and not a.attisdropped\n )\n ]::smallint[]\n ) then\n raise exception 'profile table primary key must be exactly id';\n end if;\n\n if not exists (\n select 1\n from pg_constraint c\n where c.conrelid = format('public.%I', profile_table)::regclass\n and c.confrelid = 'auth.users'::regclass\n and c.contype = 'f'\n and c.confdeltype = 'c'\n and c.conkey = array[\n (\n select a.attnum\n from pg_attribute a\n where a.attrelid = format('public.%I', profile_table)::regclass\n and a.attname = 'id'\n and not a.attisdropped\n )\n ]::smallint[]\n and c.confkey = array[\n (\n select a.attnum\n from pg_attribute a\n where a.attrelid = 'auth.users'::regclass\n and a.attname = 'id'\n and not a.attisdropped\n )\n ]::smallint[]\n ) then\n raise exception 'profile id must reference auth.users(id) with cascade delete';\n end if;\n\n if not exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = 'id'\n and c.udt_name = 'uuid'\n and c.is_nullable = 'NO'\n ) then\n raise exception 'profile id column must be uuid not null';\n end if;\n\n if not exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = 'created_at'\n and c.data_type = 'timestamp with time zone'\n and c.is_nullable = 'NO'\n and c.column_default = 'now()'\n ) then\n raise exception 'profile created_at column must be timestamptz not null default now()';\n end if;\n\n if not exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = 'updated_at'\n and c.data_type = 'timestamp with time zone'\n and c.is_nullable = 'NO'\n and c.column_default = 'now()'\n ) then\n raise exception 'profile updated_at column must be timestamptz not null default now()';\n end if;\n\n foreach expected_column in array configured_columns loop\n if not exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = expected_column\n and c.data_type = 'text'\n ) then\n raise exception 'missing configured managed profile column %', expected_column;\n end if;\n end loop;\n\n foreach expected_column in array managed_columns loop\n if not expected_column = any(configured_columns) and exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = expected_column\n ) then\n raise exception 'stale managed profile column % exists', expected_column;\n end if;\n end loop;\n\n if exists (\n select 1\n from information_schema.columns\n where table_schema = 'public'\n and table_name = profile_table\n and column_name = 'role'\n ) then\n raise exception 'generated role column must not exist';\n end if;\n\n if not exists (\n select 1\n from pg_class c\n join pg_namespace n on n.oid = c.relnamespace\n where n.nspname = 'public'\n and c.relname = profile_table\n and c.relrowsecurity\n ) then\n raise exception 'profile table RLS is not enabled';\n end if;\n\n if not exists (\n select 1\n from pg_policy p\n join pg_class c on c.oid = p.polrelid\n join pg_namespace n on n.oid = c.relnamespace\n where n.nspname = 'public'\n and c.relname = profile_table\n and p.polname = '${selectPolicy}'\n and p.polcmd = 'r'\n and p.polroles = array['authenticated'::regrole]::oid[]\n and regexp_replace(lower(pg_get_expr(p.polqual, p.polrelid)), '[[:space:]()]', '', 'g') in (\n 'auth.uid=id',\n 'auth.uid=' || profile_table || '.id',\n 'selectauth.uidasuid=id',\n 'selectauth.uidasuid=' || profile_table || '.id'\n )\n and p.polwithcheck is null\n ) then\n raise exception 'own-profile SELECT policy is missing or has unsafe definition';\n end if;\n\n if not exists (\n select 1\n from pg_policy p\n join pg_class c on c.oid = p.polrelid\n join pg_namespace n on n.oid = c.relnamespace\n where n.nspname = 'public'\n and c.relname = profile_table\n and p.polname = '${updatePolicy}'\n and p.polcmd = 'w'\n and p.polroles = array['authenticated'::regrole]::oid[]\n and regexp_replace(lower(pg_get_expr(p.polqual, p.polrelid)), '[[:space:]()]', '', 'g') in (\n 'auth.uid=id',\n 'auth.uid=' || profile_table || '.id',\n 'selectauth.uidasuid=id',\n 'selectauth.uidasuid=' || profile_table || '.id'\n )\n and regexp_replace(lower(pg_get_expr(p.polwithcheck, p.polrelid)), '[[:space:]()]', '', 'g') in (\n 'auth.uid=id',\n 'auth.uid=' || profile_table || '.id',\n 'selectauth.uidasuid=id',\n 'selectauth.uidasuid=' || profile_table || '.id'\n )\n ) then\n raise exception 'own-profile UPDATE policy is missing or has unsafe definition';\n end if;\n\n if exists (\n select 1\n from pg_policy p\n join pg_class c on c.oid = p.polrelid\n join pg_namespace n on n.oid = c.relnamespace\n where n.nspname = 'public'\n and c.relname = profile_table\n and p.polname not in ('${selectPolicy}', '${updatePolicy}')\n ) then\n raise exception 'unexpected profile table RLS policy exists';\n end if;\n\n if has_any_column_privilege('anon', format('public.%I', profile_table), 'SELECT')\n or has_any_column_privilege('anon', format('public.%I', profile_table), 'INSERT')\n or has_any_column_privilege('anon', format('public.%I', profile_table), 'UPDATE')\n or has_any_column_privilege('anon', format('public.%I', profile_table), 'REFERENCES')\n or has_table_privilege('anon', format('public.%I', profile_table), 'DELETE') then\n raise exception 'anon must not have profile table privileges';\n end if;\n\n if not has_table_privilege('authenticated', format('public.%I', profile_table), 'SELECT') then\n raise exception 'authenticated role must have profile table SELECT privilege';\n end if;\n\n if has_any_column_privilege('authenticated', format('public.%I', profile_table), 'INSERT')\n or has_any_column_privilege('authenticated', format('public.%I', profile_table), 'REFERENCES')\n or has_table_privilege('authenticated', format('public.%I', profile_table), 'DELETE') then\n raise exception 'authenticated role must not have profile table INSERT, DELETE, or REFERENCES privilege';\n end if;\n\n foreach expected_column in array ${updateGrantArray} loop\n if not has_column_privilege('authenticated', format('public.%I', profile_table), expected_column, 'UPDATE') then\n raise exception 'authenticated role is missing UPDATE privilege on configured profile column %', expected_column;\n end if;\n end loop;\n\n foreach expected_column in array (\n select array_agg(c.column_name::text order by c.ordinal_position)\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n ) loop\n if not expected_column = any(configured_columns)\n and has_column_privilege('authenticated', format('public.%I', profile_table), expected_column, 'UPDATE') then\n raise exception 'authenticated role has unexpected UPDATE privilege on profile column %', expected_column;\n end if;\n end loop;\n\n foreach expected_column in array ${protectedUpdateColumnsArray} loop\n if exists (\n select 1\n from information_schema.columns c\n where c.table_schema = 'public'\n and c.table_name = profile_table\n and c.column_name = expected_column\n ) and has_column_privilege('authenticated', format('public.%I', profile_table), expected_column, 'UPDATE') then\n raise exception 'authenticated role must not have UPDATE privilege on protected profile column %', expected_column;\n end if;\n end loop;\n\n${triggerCheck}\n\n if '${profileModel.createStrategy}' = 'trigger' then\n if not exists (\n select 1\n from pg_proc p\n join pg_namespace n on n.oid = p.pronamespace\n where n.nspname = 'public'\n and p.proname = '${functionName}'\n and p.prosecdef\n and p.prorettype = 'trigger'::regtype\n and p.proconfig @> array['search_path=pg_catalog, pg_temp']\n ) then\n raise exception 'generated trigger function is missing required SECURITY DEFINER properties';\n end if;\n\n if exists (\n select 1\n from pg_proc p\n join pg_namespace n on n.oid = p.pronamespace\n cross join lateral aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) acl\n where n.nspname = 'public'\n and p.proname = '${functionName}'\n and acl.privilege_type = 'EXECUTE'\n and (\n acl.grantee = 0\n or acl.grantee = 'anon'::regrole\n or acl.grantee = 'authenticated'::regrole\n )\n ) then\n raise exception 'generated trigger function execute privilege must be revoked from PUBLIC, anon, and authenticated';\n end if;\n end if;\nend;\n$$;`;\n}\n\nfunction getStatusScript(args: {\n defaultNamespace: string;\n supabaseProjectId: string | null;\n supabaseLocalEnabled: boolean;\n profileModel: ResolvedProfileModel;\n}): string {\n const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;\n const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';\n const profileVerificationSql = profileModel.enabled\n ? getSupabaseProfileVerificationSql(profileModel)\n : '';\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\n\nif [[ -f \"\\${ROOT_DIR}/.env\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ROOT_DIR}/.env\"\n set +a\nfi\n\nNAMESPACE=\"\\${ANKH_NAMESPACE:-${defaultNamespace}}\"\nAUTH_RUNTIME_MODE=\"\\${AUTH_RUNTIME_MODE:-local}\"\nSUPABASE_LOCAL_ENABLED=\"\\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}\"\nSUPABASE_SECRET_SYNC_ENABLED=\"\\${SUPABASE_SECRET_SYNC_ENABLED:-true}\"\nSUPABASE_PROJECT_DIR=\"\\${SUPABASE_PROJECT_DIR:-\\${ROOT_DIR}}\"\nEXPECTED_SUPABASE_PROJECT_ID=\"${supabaseProjectId ?? ''}\"\nSUPABASE_PROFILE_ENABLED=\"${profileModel.enabled ? 'true' : 'false'}\"\nSUPABASE_PROFILE_RECONCILE_FILE=\"\\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql\"\nexport EXPECTED_SUPABASE_PROJECT_ID\n\nreject_supabase_project_id_override() {\n if [[ -n \"\\${SUPABASE_PROJECT_ID:-}\" ]]; then\n echo \"SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.\"\n echo \"Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead.\"\n return 1\n fi\n\n unset SUPABASE_PROJECT_ID\n}\n\nrequire_expected_supabase_project_identity() {\n if [[ -z \"\\${EXPECTED_SUPABASE_PROJECT_ID}\" ]]; then\n echo \"Cannot run local Supabase infrastructure: expected Supabase project identity is empty.\"\n echo \"Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services.\"\n return 1\n fi\n}\n\nif ! command -v kubectl >/dev/null 2>&1; then\n echo \"kubectl is required but not installed.\"\n exit 1\nfi\n\nif ! kubectl config current-context >/dev/null 2>&1; then\n echo \"No active kubectl context is configured.\"\n echo \"Run ./scripts/up.sh to start Minikube and apply manifests.\"\n exit 1\nfi\n\nif ! kubectl cluster-info >/dev/null 2>&1; then\n CONTEXT=\"$(kubectl config current-context 2>/dev/null || echo \"unknown\")\"\n echo \"Cannot reach Kubernetes API server for context '\\${CONTEXT}'.\"\n echo \"Run ./scripts/up.sh or verify your kubeconfig context.\"\n exit 1\nfi\n\nif ! kubectl get namespace \"\\${NAMESPACE}\" >/dev/null 2>&1; then\n echo \"Namespace '\\${NAMESPACE}' was not found in the current cluster.\"\n echo \"Run ./scripts/up.sh to create it and apply resources.\"\n exit 1\nfi\n\nkubectl get all -n \"\\${NAMESPACE}\"\n\necho\necho \"Runtime auth checks:\"\necho \"- AUTH_RUNTIME_MODE=\\${AUTH_RUNTIME_MODE}\"\necho \"- SUPABASE_SECRET_SYNC_ENABLED=\\${SUPABASE_SECRET_SYNC_ENABLED}\"\n\nif kubectl -n \"\\${NAMESPACE}\" get configmap app-runtime-auth-env >/dev/null 2>&1; then\n echo \"- configmap/app-runtime-auth-env: present\"\nelse\n echo \"- configmap/app-runtime-auth-env: missing\"\nfi\n\nif [[ \"\\${SUPABASE_SECRET_SYNC_ENABLED}\" == \"true\" ]]; then\n if kubectl -n \"\\${NAMESPACE}\" get secret supabase-auth-secrets >/dev/null 2>&1; then\n echo \"- secret/supabase-auth-secrets: present\"\n\n has_secret_key() {\n local key=\"$1\"\n local value\n value=\"$(kubectl -n \"\\${NAMESPACE}\" get secret supabase-auth-secrets -o \"jsonpath={.data.\\${key}}\" 2>/dev/null || true)\"\n [[ -n \"\\${value}\" ]]\n }\n\n required_secret_keys=(\n SUPABASE_URL\n SUPABASE_ANON_KEY\n SUPABASE_SERVICE_ROLE_KEY\n SUPABASE_JWT_SECRET\n EXPO_PUBLIC_SUPABASE_URL\n EXPO_PUBLIC_SUPABASE_ANON_KEY\n )\n\n for key in \"\\${required_secret_keys[@]}\"; do\n if has_secret_key \"\\${key}\"; then\n echo \" - \\${key}: present\"\n else\n echo \" - \\${key}: missing\"\n fi\n done\n else\n echo \"- secret/supabase-auth-secrets: missing\"\n fi\nelse\n echo \"- secret/supabase-auth-secrets: skipped (SUPABASE_SECRET_SYNC_ENABLED=false)\"\nfi\n\nversion_ge() {\n local actual=\"$1\"\n local minimum=\"$2\"\n\n local actual_major actual_minor actual_patch\n local minimum_major minimum_minor minimum_patch\n IFS=. read -r actual_major actual_minor actual_patch <<< \"\\${actual}\"\n IFS=. read -r minimum_major minimum_minor minimum_patch <<< \"\\${minimum}\"\n\n actual_major=\"\\${actual_major:-0}\"\n actual_minor=\"\\${actual_minor:-0}\"\n actual_patch=\"\\${actual_patch:-0}\"\n minimum_major=\"\\${minimum_major:-0}\"\n minimum_minor=\"\\${minimum_minor:-0}\"\n minimum_patch=\"\\${minimum_patch:-0}\"\n\n (( actual_major > minimum_major )) && return 0\n (( actual_major < minimum_major )) && return 1\n (( actual_minor > minimum_minor )) && return 0\n (( actual_minor < minimum_minor )) && return 1\n (( actual_patch >= minimum_patch ))\n}\n\nrequire_supabase_cli_capabilities() {\n if ! command -v supabase >/dev/null 2>&1; then\n echo \"supabase CLI is required for local schema status checks.\"\n echo \"Install or upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n return 1\n fi\n\n local version\n version=\"$(supabase --version | awk '{print $NF}')\"\n if ! version_ge \"\\${version}\" \"2.106.0\"; then\n echo \"supabase CLI >= 2.106.0 is required; found \\${version}.\"\n echo \"Upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n return 1\n fi\n\n if ! supabase status --help 2>/dev/null | grep -Fq -- \"--workdir\"; then\n echo \"supabase CLI does not support required global --workdir flag.\"\n return 1\n fi\n\n if ! supabase migration up --help 2>/dev/null | grep -Fq -- \"--local\"; then\n echo \"supabase CLI does not support required migration up --local command.\"\n return 1\n fi\n\n if ! command -v psql >/dev/null 2>&1; then\n echo \"psql is required for local schema status checks.\"\n return 1\n fi\n}\n\nread_env_value() {\n local source=\"$1\"\n local key=\"$2\"\n local line\n line=\"$(printf '%s\\\\n' \"\\${source}\" | awk -v key=\"\\${key}\" '\n $0 ~ \"^(export[[:space:]]+)?\" key \"=\" {print}\n ' | tail -n1)\"\n\n if [[ -z \"\\${line}\" ]]; then\n echo \"\"\n return\n fi\n\n line=\"\\${line#export }\"\n line=\"\\${line#\\${key}=}\"\n line=\"\\${line%\\\\\"}\"\n line=\"\\${line#\\\\\"}\"\n printf '%s' \"\\${line}\"\n}\n\nrun_supabase_sql() {\n local sql=\"$1\"\n local label=\"$2\"\n local tmp_file\n tmp_file=\"$(mktemp)\"\n printf '%s\\\\n' \"\\${sql}\" > \"\\${tmp_file}\"\n\n if psql \"\\${SUPABASE_DB_URL}\" -v ON_ERROR_STOP=1 -q -f \"\\${tmp_file}\" >/dev/null; then\n rm -f \"\\${tmp_file}\"\n return 0\n fi\n\n rm -f \"\\${tmp_file}\"\n echo \"- \\${label}: failed\"\n return 1\n}\n\nvalidate_supabase_project_identity() {\n local config_file=\"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\"\n\n if [[ -z \"\\${EXPECTED_SUPABASE_PROJECT_ID}\" ]]; then\n return 0\n fi\n\n if [[ ! -f \"\\${config_file}\" ]]; then\n echo \"- local Supabase project identity: config missing at \\${config_file}\"\n return 1\n fi\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \"- local Supabase project identity: python3 is required to read \\${config_file}\"\n return 1\n fi\n\n python3 - \"\\${config_file}\" <<'PY'\nimport os\nimport re\nimport sys\n\n\ndef read_top_level_project_id(config_path):\n key_re = re.compile(r'^\\\\s*project_id\\\\s*=\\\\s*(.*?)\\\\s*(?:#.*)?$')\n section_re = re.compile(r'^\\\\s*\\\\[[^\\\\]]+\\\\]\\\\s*$')\n\n with open(config_path, 'r', encoding='utf-8') as f:\n for line in f:\n if section_re.match(line):\n return None\n match = key_re.match(line)\n if not match:\n continue\n raw = match.group(1).strip()\n quoted = re.match(r'^[\"\\\\']([^\"\\\\']*)[\"\\\\']$', raw)\n return quoted.group(1) if quoted else raw\n\n return None\n\n\nexpected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']\nactual = read_top_level_project_id(sys.argv[1])\n\nif actual == expected:\n raise SystemExit(0)\n\nfound = 'missing' if actual is None else f'\"{actual}\"'\nprint('Supabase project identity mismatch.', file=sys.stderr)\nprint(f'Expected \"{expected}\", found {found}.', file=sys.stderr)\nprint('Destroy the invalid local stack and run Infra Up again.', file=sys.stderr)\nraise SystemExit(1)\nPY\n}\n\ncheck_immutable_migrations_applied() {\n local migrations_dir=\"\\${SUPABASE_PROJECT_DIR}/supabase/migrations\"\n local applied_versions\n applied_versions=\"$(psql \"\\${SUPABASE_DB_URL}\" -v ON_ERROR_STOP=1 -Atc \"select version from supabase_migrations.schema_migrations\" 2>/dev/null || true)\"\n\n if [[ -z \"\\${applied_versions}\" ]]; then\n applied_versions=\"\"\n fi\n\n if [[ ! -d \"\\${migrations_dir}\" ]]; then\n return 0\n fi\n\n local file\n local version\n local pending=0\n for file in \"\\${migrations_dir}\"/*.sql; do\n [[ -e \"\\${file}\" ]] || continue\n version=\"$(basename \"\\${file}\")\"\n version=\"\\${version%%_*}\"\n if ! printf '%s\\\\n' \"\\${applied_versions}\" | grep -Fxq \"\\${version}\"; then\n echo \"- immutable migrations: pending \\${file}\"\n pending=1\n fi\n done\n\n return \"\\${pending}\"\n}\n\nif [[ \"\\${AUTH_RUNTIME_MODE}\" == \"local\" && \"\\${SUPABASE_LOCAL_ENABLED}\" == \"true\" ]]; then\n echo\n echo \"Local Supabase database checks:\"\n status_failed=0\n\n if ! require_expected_supabase_project_identity; then\n status_failed=1\n elif ! reject_supabase_project_id_override; then\n status_failed=1\n elif require_supabase_cli_capabilities; then\n if ! validate_supabase_project_identity; then\n status_failed=1\n elif supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status >/dev/null 2>&1; then\n STATUS_ENV=\"$(supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status -o env)\"\n SUPABASE_DB_URL=\"$(read_env_value \"\\${STATUS_ENV}\" DB_URL)\"\n if [[ -z \"\\${SUPABASE_DB_URL}\" ]]; then\n echo \"- immutable migrations: status unavailable (DB_URL missing from supabase status)\"\n status_failed=1\n elif check_immutable_migrations_applied; then\n echo \"- immutable migrations: applied\"\n else\n echo \"- immutable migrations: pending\"\n status_failed=1\n fi\n\n if [[ \"\\${SUPABASE_PROFILE_ENABLED}\" == \"true\" ]]; then\n if [[ -f \"\\${SUPABASE_PROFILE_RECONCILE_FILE}\" ]]; then\n PROFILE_STATUS_SQL=$(cat <<'SQL'\n${profileStatusSql}\nSQL\n)\n PROFILE_VERIFY_SQL=$(cat <<'SQL'\n${profileVerificationSql}\nSQL\n)\n\n if run_supabase_sql \"\\${PROFILE_STATUS_SQL}\" \"profile reconciliation\"; then\n echo \"- profile reconciliation: applied, checksum matches\"\n else\n echo \"- profile reconciliation: pending or stale\"\n status_failed=1\n fi\n\n if run_supabase_sql \"\\${PROFILE_VERIFY_SQL}\" \"profile schema\"; then\n echo \"- profile schema: verified\"\n else\n echo \"- profile schema: drift detected\"\n status_failed=1\n fi\n else\n echo \"- profile reconciliation: generated file missing at \\${SUPABASE_PROFILE_RECONCILE_FILE}\"\n echo \"- profile schema: not verified\"\n status_failed=1\n fi\n else\n PROFILE_DISABLED_SQL=$(cat <<'SQL'\n${getSupabaseProfileDisabledGuardSql()}\nSQL\n)\n if run_supabase_sql \"\\${PROFILE_DISABLED_SQL}\" \"profile disabled-state\"; then\n echo \"- profile reconciliation: skipped (no profile table configured)\"\n echo \"- profile schema: skipped (no profile table configured)\"\n else\n echo \"- profile reconciliation: stale generated state exists for removed profile table\"\n echo \"- profile schema: not verified\"\n status_failed=1\n fi\n fi\n else\n echo \"- local Supabase stack: not running or not reachable from \\${SUPABASE_PROJECT_DIR}\"\n status_failed=1\n fi\n else\n status_failed=1\n fi\n\n if [[ \"\\${status_failed}\" -ne 0 ]]; then\n exit \"\\${status_failed}\"\n fi\nfi\n`;\n}\n\nfunction getSupabaseLocalEnvScript(args: {\n supabaseProjectId: string | null;\n supabaseLocalPorts: SupabaseLocalPorts;\n profileModel: ResolvedProfileModel;\n}): string {\n const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;\n const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';\n const profileVerificationSql = profileModel.enabled\n ? getSupabaseProfileVerificationSql(profileModel)\n : '';\n\n return `#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"\nROOT_DIR=\"$(cd \"\\${SCRIPT_DIR}/..\" && pwd)\"\nENV_FILE=\"\\${ROOT_DIR}/.env\"\nENV_EXAMPLE_FILE=\"\\${ROOT_DIR}/.env.example\"\n\nif [[ -f \"\\${ENV_FILE}\" ]]; then\n set -a\n # shellcheck disable=SC1090\n source \"\\${ENV_FILE}\"\n set +a\nfi\n\nAPP_SOURCE_DIR=\"\\${APP_SOURCE_DIR:-$(cd \"\\${ROOT_DIR}/../..\" && pwd)}\"\nSUPABASE_PROJECT_DIR=\"\\${SUPABASE_PROJECT_DIR:-\\${ROOT_DIR}}\"\nEXPECTED_SUPABASE_PROJECT_ID=\"${supabaseProjectId ?? ''}\"\nAPP_SUPABASE_ENV_FILE=\"\\${APP_SUPABASE_ENV_FILE:-\\${APP_SOURCE_DIR}/.env.local}\"\nSUPABASE_PROFILE_ENABLED=\"${profileModel.enabled ? 'true' : 'false'}\"\nSUPABASE_PROFILE_RECONCILE_FILE=\"\\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql\"\nexport EXPECTED_SUPABASE_PROJECT_ID\n\nreject_supabase_project_id_override() {\n if [[ -n \"\\${SUPABASE_PROJECT_ID:-}\" ]]; then\n echo \"SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.\"\n echo \"Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead.\"\n exit 1\n fi\n\n unset SUPABASE_PROJECT_ID\n}\n\nrequire_expected_supabase_project_identity() {\n if [[ -z \"\\${EXPECTED_SUPABASE_PROJECT_ID}\" ]]; then\n echo \"Cannot run local Supabase infrastructure: expected Supabase project identity is empty.\"\n echo \"Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services.\"\n exit 1\n fi\n}\n\nSUPABASE_LOCAL_PORT_BASE=\"\\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}\"\nSUPABASE_LOCAL_SHADOW_PORT=\"\\${SUPABASE_LOCAL_SHADOW_PORT:-\\${SUPABASE_LOCAL_PORT_BASE}}\"\nSUPABASE_LOCAL_API_PORT=\"\\${SUPABASE_LOCAL_API_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 1))}\"\nSUPABASE_LOCAL_DB_PORT=\"\\${SUPABASE_LOCAL_DB_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 2))}\"\nSUPABASE_LOCAL_STUDIO_PORT=\"\\${SUPABASE_LOCAL_STUDIO_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 3))}\"\nSUPABASE_LOCAL_INBUCKET_PORT=\"\\${SUPABASE_LOCAL_INBUCKET_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 4))}\"\nSUPABASE_LOCAL_ANALYTICS_PORT=\"\\${SUPABASE_LOCAL_ANALYTICS_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 5))}\"\nexport SUPABASE_LOCAL_PORT_BASE\nexport SUPABASE_LOCAL_SHADOW_PORT\nexport SUPABASE_LOCAL_API_PORT\nexport SUPABASE_LOCAL_DB_PORT\nexport SUPABASE_LOCAL_STUDIO_PORT\nexport SUPABASE_LOCAL_INBUCKET_PORT\nexport SUPABASE_LOCAL_ANALYTICS_PORT\nversion_ge() {\n local actual=\"$1\"\n local minimum=\"$2\"\n\n local actual_major actual_minor actual_patch\n local minimum_major minimum_minor minimum_patch\n IFS=. read -r actual_major actual_minor actual_patch <<< \"\\${actual}\"\n IFS=. read -r minimum_major minimum_minor minimum_patch <<< \"\\${minimum}\"\n\n actual_major=\"\\${actual_major:-0}\"\n actual_minor=\"\\${actual_minor:-0}\"\n actual_patch=\"\\${actual_patch:-0}\"\n minimum_major=\"\\${minimum_major:-0}\"\n minimum_minor=\"\\${minimum_minor:-0}\"\n minimum_patch=\"\\${minimum_patch:-0}\"\n\n (( actual_major > minimum_major )) && return 0\n (( actual_major < minimum_major )) && return 1\n (( actual_minor > minimum_minor )) && return 0\n (( actual_minor < minimum_minor )) && return 1\n (( actual_patch >= minimum_patch ))\n}\n\nrequire_supabase_cli_capabilities() {\n if ! command -v supabase >/dev/null 2>&1; then\n echo \"supabase CLI is required but not installed.\"\n echo \"Install or upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n exit 1\n fi\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \"python3 is required to reconcile supabase/config.toml before local Supabase startup.\"\n exit 1\n fi\n\n local version\n version=\"$(supabase --version | awk '{print $NF}')\"\n if ! version_ge \"\\${version}\" \"2.106.0\"; then\n echo \"supabase CLI >= 2.106.0 is required; found \\${version}.\"\n echo \"Upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n exit 1\n fi\n\n if ! supabase status --help 2>/dev/null | grep -Fq -- \"--workdir\"; then\n echo \"supabase CLI does not support required global --workdir flag.\"\n echo \"Upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n exit 1\n fi\n\n if ! supabase migration up --help 2>/dev/null | grep -Fq -- \"--local\"; then\n echo \"supabase CLI does not support required migration up --local command.\"\n echo \"Upgrade: https://supabase.com/docs/guides/local-development/cli/getting-started\"\n exit 1\n fi\n\n if ! command -v psql >/dev/null 2>&1; then\n echo \"psql is required but not installed.\"\n exit 1\n fi\n}\n\nformat_command() {\n printf '%q ' \"$@\"\n}\n\nrun_checked_command() {\n local label=\"$1\"\n local sql_file=\"$2\"\n shift 2\n local status\n set +e\n \"$@\"\n status=$?\n set -e\n\n if [[ \"\\${status}\" -ne 0 ]]; then\n echo \"\\${label} failed.\"\n if [[ -n \"\\${sql_file}\" ]]; then\n echo \"SQL file: \\${sql_file}\"\n fi\n echo \"Supabase project workdir: \\${SUPABASE_PROJECT_DIR}\"\n echo \"Command: $(format_command \"$@\")\"\n echo \"Exit status: \\${status}\"\n exit \"\\${status}\"\n fi\n}\n\nrun_checked_sql_file() {\n local label=\"$1\"\n local sql_file=\"$2\"\n run_checked_command \\\n \"\\${label}\" \\\n \"\\${sql_file}\" \\\n psql \"\\${SUPABASE_DB_URL}\" -v ON_ERROR_STOP=1 -q -f \"\\${sql_file}\"\n}\n\nrequire_expected_supabase_project_identity\nreject_supabase_project_id_override\nrequire_supabase_cli_capabilities\n\nif [[ ! -d \"\\${SUPABASE_PROJECT_DIR}\" ]]; then\n echo \"Supabase project directory does not exist: \\${SUPABASE_PROJECT_DIR}\"\n exit 1\nfi\n\nconfigure_supabase_local_ports() {\n local config_file=\"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\"\n\n python3 - \"\\${config_file}\" <<'PY'\nimport os\nimport re\nimport sys\nfrom dataclasses import dataclass\n\n\n@dataclass(frozen=True)\nclass Patch:\n section: str\n key: str\n value: str\n\n\ndef _env_required(name):\n value = os.environ.get(name)\n if value is None or value == \"\":\n raise RuntimeError(f\"Missing required env var: {name}\")\n return value\n\n\ndef _is_section_header(line):\n return re.match(r\"^\\\\s*\\\\[[^\\\\]]+\\\\]\\\\s*$\", line) is not None\n\n\ndef _section_name(line):\n match = re.match(r\"^\\\\s*\\\\[([^\\\\]]+)\\\\]\\\\s*$\", line)\n return match.group(1) if match else None\n\n\ndef _patch_section(lines, section, key, value):\n current_section = None\n section_start = None\n\n for i, line in enumerate(lines):\n if _is_section_header(line):\n current_section = _section_name(line)\n if current_section == section and section_start is None:\n section_start = i\n\n if section_start is None:\n if lines and not lines[-1].endswith(\"\\\\n\"):\n lines[-1] = lines[-1] + \"\\\\n\"\n if lines and lines[-1].strip() != \"\":\n lines.append(\"\\\\n\")\n lines.append(f\"[{section}]\\\\n\")\n lines.append(f\"{key} = {value}\\\\n\")\n return lines\n\n section_end = len(lines)\n for j in range(section_start + 1, len(lines)):\n if _is_section_header(lines[j]):\n section_end = j\n break\n\n key_re = re.compile(rf\"^(\\\\s*){re.escape(key)}\\\\s*=\\\\s*(.*?)(\\\\s*(#.*)?)?$\")\n for k in range(section_start + 1, section_end):\n match = key_re.match(lines[k])\n if match:\n indent = match.group(1) or \"\"\n trailing = match.group(3) or \"\"\n suffix = trailing.rstrip(\"\\\\n\")\n lines[k] = f\"{indent}{key} = {value}{suffix}\\\\n\"\n return lines\n\n insert_at = section_end\n while insert_at > section_start + 1 and lines[insert_at - 1].strip() == \"\":\n insert_at -= 1\n lines.insert(insert_at, f\"{key} = {value}\\\\n\")\n return lines\n\n\ndef main():\n if len(sys.argv) != 2:\n raise RuntimeError(\"Usage: patch_config.py <supabase/config.toml>\")\n\n config_path = sys.argv[1]\n with open(config_path, \"r\", encoding=\"utf-8\") as f:\n lines = f.readlines()\n\n patches = [\n Patch(\"api\", \"port\", _env_required(\"SUPABASE_LOCAL_API_PORT\")),\n Patch(\"db\", \"port\", _env_required(\"SUPABASE_LOCAL_DB_PORT\")),\n Patch(\"db\", \"shadow_port\", _env_required(\"SUPABASE_LOCAL_SHADOW_PORT\")),\n Patch(\"studio\", \"port\", _env_required(\"SUPABASE_LOCAL_STUDIO_PORT\")),\n Patch(\"inbucket\", \"port\", _env_required(\"SUPABASE_LOCAL_INBUCKET_PORT\")),\n Patch(\"analytics\", \"port\", _env_required(\"SUPABASE_LOCAL_ANALYTICS_PORT\")),\n ]\n\n for p in patches:\n lines = _patch_section(lines, p.section, p.key, p.value)\n\n with open(config_path, \"w\", encoding=\"utf-8\") as f:\n f.writelines(lines)\n\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\nPY\n}\n\nwrite_supabase_project_identity_for_new_config() {\n local config_file=\"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\"\n\n require_expected_supabase_project_identity\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \"python3 is required to write Supabase project_id in \\${config_file}.\"\n exit 1\n fi\n\n python3 - \"\\${config_file}\" <<'PY'\nimport os\nimport re\nimport sys\n\n\ndef write_top_level_project_id(config_path, expected):\n key_re = re.compile(r'^(\\\\s*)project_id\\\\s*=\\\\s*(.*?)(\\\\s*(#.*)?)?$')\n section_re = re.compile(r'^\\\\s*\\\\[[^\\\\]]+\\\\]\\\\s*$')\n\n with open(config_path, 'r', encoding='utf-8') as f:\n lines = f.readlines()\n\n section_start = len(lines)\n for i, line in enumerate(lines):\n if section_re.match(line):\n section_start = i\n break\n\n for i in range(section_start):\n match = key_re.match(lines[i])\n if not match:\n continue\n indent = match.group(1) or ''\n trailing = (match.group(3) or '').rstrip('\\\\n')\n lines[i] = f'{indent}project_id = \"{expected}\"{trailing}\\\\n'\n break\n else:\n insert_at = section_start\n lines.insert(insert_at, f'project_id = \"{expected}\"\\\\n')\n\n with open(config_path, 'w', encoding='utf-8') as f:\n f.writelines(lines)\n\n\nwrite_top_level_project_id(sys.argv[1], os.environ['EXPECTED_SUPABASE_PROJECT_ID'])\nPY\n}\n\nvalidate_supabase_project_identity() {\n local config_file=\"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\"\n\n require_expected_supabase_project_identity\n\n if [[ ! -f \"\\${config_file}\" ]]; then\n echo \"Supabase project identity mismatch.\"\n printf 'Expected \"%s\", found missing.\\n' \"\\${EXPECTED_SUPABASE_PROJECT_ID}\"\n echo \"The existing local Supabase project belongs to a different identity.\"\n echo \"Destroy the invalid local stack and its project-owned resources, then run Infra Up again.\"\n exit 1\n fi\n\n if ! command -v python3 >/dev/null 2>&1; then\n echo \"python3 is required to read Supabase project_id in \\${config_file}.\"\n exit 1\n fi\n\n python3 - \"\\${config_file}\" <<'PY'\nimport os\nimport re\nimport sys\n\n\ndef read_top_level_project_id(config_path):\n key_re = re.compile(r'^\\\\s*project_id\\\\s*=\\\\s*(.*?)\\\\s*(?:#.*)?$')\n section_re = re.compile(r'^\\\\s*\\\\[[^\\\\]]+\\\\]\\\\s*$')\n\n with open(config_path, 'r', encoding='utf-8') as f:\n for line in f:\n if section_re.match(line):\n return None\n match = key_re.match(line)\n if not match:\n continue\n raw = match.group(1).strip()\n quoted = re.match(r'^[\"\\\\']([^\"\\\\']*)[\"\\\\']$', raw)\n return quoted.group(1) if quoted else raw\n\n return None\n\n\nexpected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']\nactual = read_top_level_project_id(sys.argv[1])\n\nif actual == expected:\n raise SystemExit(0)\n\nfound = 'missing' if actual is None else f'\"{actual}\"'\nprint('Supabase project identity mismatch.', file=sys.stderr)\nprint(f'Expected \"{expected}\", found {found}.', file=sys.stderr)\nprint('The existing local Supabase project belongs to a different identity.', file=sys.stderr)\nprint('Destroy the invalid local stack and its project-owned resources, then run Infra Up again.', file=sys.stderr)\nraise SystemExit(1)\nPY\n}\n\nassert_supabase_local_ports_available() {\n local project_label=\"\\${EXPECTED_SUPABASE_PROJECT_ID}\"\n\n ANKH_PROJECT_LABEL=\"\\${project_label}\" python3 - <<'PY'\nimport os\nimport socket\nimport sys\n\n\nPORT_KEYS = [\n \"SUPABASE_LOCAL_SHADOW_PORT\",\n \"SUPABASE_LOCAL_API_PORT\",\n \"SUPABASE_LOCAL_DB_PORT\",\n \"SUPABASE_LOCAL_STUDIO_PORT\",\n \"SUPABASE_LOCAL_INBUCKET_PORT\",\n \"SUPABASE_LOCAL_ANALYTICS_PORT\",\n]\n\n\ndef read_port(key):\n value = os.environ.get(key, \"\")\n try:\n port = int(value)\n except ValueError:\n raise RuntimeError(f\"{key} must be an integer, got {value!r}\") from None\n\n if port < 1 or port > 65535:\n raise RuntimeError(f\"{key} must be between 1 and 65535, got {port}\")\n\n return port\n\n\ndef can_bind(port):\n with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:\n try:\n sock.bind((\"0.0.0.0\", port))\n except OSError:\n return False\n return True\n\n\ndef main():\n project_label = os.environ.get(\"ANKH_PROJECT_LABEL\", \"unknown\")\n ports = [(key, read_port(key)) for key in PORT_KEYS]\n\n seen = {}\n duplicates = []\n for key, port in ports:\n existing = seen.get(port)\n if existing is not None:\n duplicates.append((key, existing, port))\n else:\n seen[port] = key\n\n blocked = [(key, port) for key, port in ports if not can_bind(port)]\n\n if not duplicates and not blocked:\n return 0\n\n print(f\"Supabase local port preflight failed for project '{project_label}'.\", file=sys.stderr)\n\n for key, existing, port in duplicates:\n print(f\"- {key} duplicates {existing} on port {port}.\", file=sys.stderr)\n\n for key, port in blocked:\n print(f\"- {key}={port} is already in use on this host.\", file=sys.stderr)\n\n print(\"Override SUPABASE_LOCAL_PORT_BASE or the specific SUPABASE_LOCAL_*_PORT values in infra/minikube/.env and retry.\", file=sys.stderr)\n return 1\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\nPY\n}\n\nif [[ ! -f \"\\${SUPABASE_PROJECT_DIR}/supabase/config.toml\" ]]; then\n echo \"No supabase/config.toml found in \\${SUPABASE_PROJECT_DIR}. Initializing Supabase project...\"\n supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" init --yes >/dev/null\n write_supabase_project_identity_for_new_config\nfi\n\nvalidate_supabase_project_identity\nconfigure_supabase_local_ports\n\nSTATUS_ENV=\"$(\n supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status -o env 2>/dev/null || true\n)\"\n\nread_env_value() {\n local key=\"$1\"\n local line\n line=\"$(printf '%s\\\\n' \"\\${STATUS_ENV}\" | awk -v key=\"\\${key}\" '\n $0 ~ \"^(export[[:space:]]+)?\" key \"=\" {print}\n ' | tail -n1)\"\n\n if [[ -z \"\\${line}\" ]]; then\n echo \"\"\n return\n fi\n\n line=\"\\${line#export }\"\n line=\"\\${line#\\${key}=}\"\n line=\"\\${line%\\\\\"}\"\n line=\"\\${line#\\\\\"}\"\n printf '%s' \"\\${line}\"\n}\n\ncheck_immutable_migrations_applied() {\n local migrations_dir=\"\\${SUPABASE_PROJECT_DIR}/supabase/migrations\"\n local applied_versions\n applied_versions=\"$(psql \"\\${SUPABASE_DB_URL}\" -v ON_ERROR_STOP=1 -Atc \"select version from supabase_migrations.schema_migrations\" 2>/dev/null || true)\"\n\n if [[ -z \"\\${applied_versions}\" ]]; then\n applied_versions=\"\"\n fi\n\n if [[ ! -d \"\\${migrations_dir}\" ]]; then\n return 0\n fi\n\n local file\n local version\n local pending=0\n for file in \"\\${migrations_dir}\"/*.sql; do\n [[ -e \"\\${file}\" ]] || continue\n version=\"$(basename \"\\${file}\")\"\n version=\"\\${version%%_*}\"\n if ! printf '%s\\\\n' \"\\${applied_versions}\" | grep -Fxq \"\\${version}\"; then\n echo \"Pending immutable Supabase migration: \\${file}\"\n pending=1\n fi\n done\n\n return \"\\${pending}\"\n}\n\nstart_supabase_local_stack() {\n if supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" start >/dev/null; then\n return 0\n fi\n\n echo \"Supabase local start failed. Stopping stale local stack and retrying once...\"\n supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" stop --no-backup >/dev/null 2>&1 || true\n\n if supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" start >/dev/null; then\n return 0\n fi\n\n echo \"Supabase local start failed after retry.\"\n return 1\n}\n\nif [[ -z \"\\${STATUS_ENV}\" ]]; then\n validate_supabase_project_identity\n assert_supabase_local_ports_available\n echo \"Supabase local stack not detected. Starting with 'supabase start'...\"\n start_supabase_local_stack\n STATUS_ENV=\"$(supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status -o env)\"\nfi\n\nSUPABASE_DB_URL=\"$(read_env_value DB_URL)\"\nif [[ -z \"\\${SUPABASE_DB_URL}\" ]]; then\n echo \"Unable to read DB_URL from 'supabase status -o env'.\"\n exit 1\nfi\n\necho \"Applying pending immutable Supabase migrations...\"\nvalidate_supabase_project_identity\nrun_checked_command \\\n \"Immutable Supabase migration application\" \\\n \"\" \\\n supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" migration up --local\n\nif ! check_immutable_migrations_applied; then\n echo \"Immutable Supabase migration application did not apply every local migration.\"\n exit 1\nfi\n\nif [[ \"\\${SUPABASE_PROFILE_ENABLED}\" == \"true\" ]]; then\n if [[ ! -f \"\\${SUPABASE_PROFILE_RECONCILE_FILE}\" ]]; then\n echo \"Generated profile reconciliation file is missing: \\${SUPABASE_PROFILE_RECONCILE_FILE}\"\n exit 1\n fi\n\n echo \"Applying generated profile reconciliation...\"\n run_checked_sql_file \\\n \"Generated profile reconciliation\" \\\n \"\\${SUPABASE_PROFILE_RECONCILE_FILE}\"\n\n PROFILE_STATUS_SQL=$(cat <<'SQL'\n${profileStatusSql}\nSQL\n)\n PROFILE_VERIFY_SQL=$(cat <<'SQL'\n${profileVerificationSql}\nSQL\n)\n\n PROFILE_STATUS_FILE=\"$(mktemp)\"\n PROFILE_VERIFY_FILE=\"$(mktemp)\"\n printf '%s\\\\n' \"\\${PROFILE_STATUS_SQL}\" > \"\\${PROFILE_STATUS_FILE}\"\n printf '%s\\\\n' \"\\${PROFILE_VERIFY_SQL}\" > \"\\${PROFILE_VERIFY_FILE}\"\n\n run_checked_sql_file \\\n \"Generated profile reconciliation checksum verification\" \\\n \"\\${PROFILE_STATUS_FILE}\"\n run_checked_sql_file \\\n \"Generated profile schema verification\" \\\n \"\\${PROFILE_VERIFY_FILE}\"\n\n rm -f \"\\${PROFILE_STATUS_FILE}\" \"\\${PROFILE_VERIFY_FILE}\"\n echo \"Generated profile reconciliation applied and schema verified.\"\nelse\n PROFILE_DISABLED_FILE=\"$(mktemp)\"\n cat > \"\\${PROFILE_DISABLED_FILE}\" <<'SQL'\n${getSupabaseProfileDisabledGuardSql()}\nSQL\n run_checked_sql_file \\\n \"Generated profile disabled-state verification\" \\\n \"\\${PROFILE_DISABLED_FILE}\"\n rm -f \"\\${PROFILE_DISABLED_FILE}\"\nfi\n\nSTATUS_ENV=\"$(supabase --workdir \"\\${SUPABASE_PROJECT_DIR}\" status -o env)\"\n\nAPI_URL=\"$(read_env_value API_URL)\"\nANON_KEY=\"$(read_env_value ANON_KEY)\"\nSERVICE_ROLE_KEY=\"$(read_env_value SERVICE_ROLE_KEY)\"\nJWT_SECRET=\"$(read_env_value JWT_SECRET)\"\n\nif [[ -z \"\\${API_URL}\" || -z \"\\${ANON_KEY}\" || -z \"\\${SERVICE_ROLE_KEY}\" || -z \"\\${JWT_SECRET}\" ]]; then\n echo \"Unable to read required Supabase values from 'supabase status -o env'.\"\n echo \"Expected API_URL, ANON_KEY, SERVICE_ROLE_KEY, JWT_SECRET.\"\n exit 1\nfi\n\nupsert_env() {\n local file=\"$1\"\n local key=\"$2\"\n local value=\"$3\"\n local tmp_file\n tmp_file=\"$(mktemp)\"\n\n awk -v key=\"\\${key}\" -v value=\"\\${value}\" '\n BEGIN { replaced = 0 }\n index($0, key \"=\") == 1 {\n print key \"=\" value\n replaced = 1\n next\n }\n { print }\n END {\n if (replaced == 0) {\n print key \"=\" value\n }\n }\n ' \"\\${file}\" > \"\\${tmp_file}\"\n\n mv \"\\${tmp_file}\" \"\\${file}\"\n}\n\nif [[ ! -f \"\\${ENV_FILE}\" ]]; then\n cp \"\\${ENV_EXAMPLE_FILE}\" \"\\${ENV_FILE}\"\nfi\n\nif [[ ! -f \"\\${APP_SUPABASE_ENV_FILE}\" ]]; then\n touch \"\\${APP_SUPABASE_ENV_FILE}\"\nfi\n\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_SECRET_SYNC_ENABLED\" \"true\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_URL\" \"\\${API_URL}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_ANON_KEY\" \"\\${ANON_KEY}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_SERVICE_ROLE_KEY\" \"\\${SERVICE_ROLE_KEY}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_JWT_SECRET\" \"\\${JWT_SECRET}\"\nupsert_env \"\\${ENV_FILE}\" \"EXPO_PUBLIC_SUPABASE_URL\" \"\\${API_URL}\"\nupsert_env \"\\${ENV_FILE}\" \"EXPO_PUBLIC_SUPABASE_ANON_KEY\" \"\\${ANON_KEY}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_PROJECT_DIR\" \"\\${SUPABASE_PROJECT_DIR}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_PORT_BASE\" \"\\${SUPABASE_LOCAL_PORT_BASE}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_SHADOW_PORT\" \"\\${SUPABASE_LOCAL_SHADOW_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_API_PORT\" \"\\${SUPABASE_LOCAL_API_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_DB_PORT\" \"\\${SUPABASE_LOCAL_DB_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_STUDIO_PORT\" \"\\${SUPABASE_LOCAL_STUDIO_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_INBUCKET_PORT\" \"\\${SUPABASE_LOCAL_INBUCKET_PORT}\"\nupsert_env \"\\${ENV_FILE}\" \"SUPABASE_LOCAL_ANALYTICS_PORT\" \"\\${SUPABASE_LOCAL_ANALYTICS_PORT}\"\n\nupsert_env \"\\${APP_SUPABASE_ENV_FILE}\" \"EXPO_PUBLIC_SUPABASE_URL\" \"\\${API_URL}\"\nupsert_env \"\\${APP_SUPABASE_ENV_FILE}\" \"EXPO_PUBLIC_SUPABASE_ANON_KEY\" \"\\${ANON_KEY}\"\nupsert_env \"\\${APP_SUPABASE_ENV_FILE}\" \"SUPABASE_URL\" \"\\${API_URL}\"\nupsert_env \"\\${APP_SUPABASE_ENV_FILE}\" \"SUPABASE_ANON_KEY\" \"\\${ANON_KEY}\"\n\necho \"Updated \\${ENV_FILE} with local Supabase credentials from \\${SUPABASE_PROJECT_DIR}.\"\necho \"Updated \\${APP_SUPABASE_ENV_FILE} with Expo Supabase credentials for local app runs.\"\necho \"If Expo is already running, restart it to pick up updated EXPO_PUBLIC_* variables.\"\necho \"Next: run ./scripts/up.sh to sync Kubernetes secrets and apply infrastructure.\"\n`;\n}\n\nfunction getDefaultAppImage(namespace: string): string {\n return `ankh/${namespace}:dev`;\n}\n\nfunction resolveSupabaseLocalPorts(namespace: string): SupabaseLocalPorts {\n const base =\n SUPABASE_LOCAL_PORT_BASE +\n resolveSupabaseLocalPortBucket(namespace) * SUPABASE_LOCAL_PORT_BUCKET_SIZE;\n\n return {\n base,\n shadow: base,\n api: base + 1,\n db: base + 2,\n studio: base + 3,\n inbucket: base + 4,\n analytics: base + 5,\n };\n}\n\nfunction resolveSupabaseLocalPortBucket(namespace: string): number {\n const rawBucket = hashProjectId(namespace) - hashProjectId(SUPABASE_LOCAL_PORT_REFERENCE_PROJECT);\n return (rawBucket + SUPABASE_LOCAL_PORT_BUCKET_COUNT) % SUPABASE_LOCAL_PORT_BUCKET_COUNT;\n}\n\nfunction hashProjectId(value: string): number {\n const source = value.trim().length > 0 ? value.trim() : 'app';\n let hash = 0;\n\n for (const char of source) {\n hash = (hash * 31 + char.charCodeAt(0)) % SUPABASE_LOCAL_PORT_BUCKET_COUNT;\n }\n\n return hash;\n}\n\nfunction getSqlTextArray(values: readonly string[]): string {\n return `array[${values.map((value) => `'${escapeSqlLiteral(value)}'`).join(', ')}]::text[]`;\n}\n\nfunction escapeSqlLiteral(value: string): string {\n return value.replaceAll(\"'\", \"''\");\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/infra",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"description": "Project-aware infrastructure generation library and executable Ankh provider for Ankhorage.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/infra#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
-
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
|
|
7
7
|
import { describe, expect, test } from 'bun:test';
|
|
8
8
|
|
|
9
|
-
import { generateInfrastructure } from '../../../../index';
|
|
9
|
+
import { generateInfrastructure as generateRawInfrastructure } from '../../../../index';
|
|
10
|
+
import { createAppManifest } from '../../../../testSupport';
|
|
10
11
|
import type { GeneratedInfrastructureFile, InfraManifestInput } from '../../../../types';
|
|
11
12
|
|
|
12
13
|
const execFile = promisify(execFileCallback);
|
|
13
14
|
const INTEGRATION_TIMEOUT_MS = 300_000;
|
|
14
15
|
const integrationTest = process.env.ANKH_SUPABASE_INTEGRATION === '1' ? test : test.skip;
|
|
15
16
|
|
|
17
|
+
function generateInfrastructure(
|
|
18
|
+
manifest: InfraManifestInput,
|
|
19
|
+
options: { namespaceHint?: string } = {},
|
|
20
|
+
): ReturnType<typeof generateRawInfrastructure> {
|
|
21
|
+
const slug = options.namespaceHint ?? 'profile-integration';
|
|
22
|
+
return generateRawInfrastructure(manifest, {
|
|
23
|
+
...options,
|
|
24
|
+
appManifest: createAppManifest(slug),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
describe('generated Supabase profile reconciliation integration', () => {
|
|
17
29
|
integrationTest(
|
|
18
30
|
'drops stale managed columns while preserving custom columns',
|
|
@@ -132,6 +144,109 @@ $$;`,
|
|
|
132
144
|
INTEGRATION_TIMEOUT_MS,
|
|
133
145
|
);
|
|
134
146
|
|
|
147
|
+
integrationTest(
|
|
148
|
+
'boots clean disabled profile and writes client env idempotently',
|
|
149
|
+
async () => {
|
|
150
|
+
const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await writeGeneratedFiles(
|
|
154
|
+
appRoot,
|
|
155
|
+
generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
|
|
156
|
+
namespaceHint,
|
|
157
|
+
}).files,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
161
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
162
|
+
|
|
163
|
+
const { stdout: statusEnv } = await execFile('supabase', [
|
|
164
|
+
'--workdir',
|
|
165
|
+
minikubeRoot,
|
|
166
|
+
'status',
|
|
167
|
+
'-o',
|
|
168
|
+
'env',
|
|
169
|
+
]);
|
|
170
|
+
const expectedUrl = readStatusEnv(statusEnv, 'API_URL');
|
|
171
|
+
const expectedAnonKey = readStatusEnv(statusEnv, 'ANON_KEY');
|
|
172
|
+
const appEnv = await readFile(path.join(appRoot, '.env.local'), 'utf8');
|
|
173
|
+
|
|
174
|
+
expect(readStatusEnv(appEnv, 'EXPO_PUBLIC_SUPABASE_URL')).toBe(expectedUrl);
|
|
175
|
+
expect(readStatusEnv(appEnv, 'EXPO_PUBLIC_SUPABASE_ANON_KEY')).toBe(expectedAnonKey);
|
|
176
|
+
} finally {
|
|
177
|
+
await cleanupIntegrationProject(appRoot, minikubeRoot);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
INTEGRATION_TIMEOUT_MS,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
integrationTest(
|
|
184
|
+
'accepts disabled profile when state table exists without auth profile state',
|
|
185
|
+
async () => {
|
|
186
|
+
const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await writeGeneratedFiles(
|
|
190
|
+
appRoot,
|
|
191
|
+
generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
|
|
192
|
+
namespaceHint,
|
|
193
|
+
}).files,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
197
|
+
await runSupabaseSql(
|
|
198
|
+
minikubeRoot,
|
|
199
|
+
`create schema if not exists ankhorage_internal;
|
|
200
|
+
create table if not exists ankhorage_internal.generated_schema_state (
|
|
201
|
+
artifact_key text primary key,
|
|
202
|
+
table_name text,
|
|
203
|
+
content_hash text,
|
|
204
|
+
updated_at timestamptz not null default now()
|
|
205
|
+
);
|
|
206
|
+
insert into ankhorage_internal.generated_schema_state (
|
|
207
|
+
artifact_key,
|
|
208
|
+
table_name,
|
|
209
|
+
content_hash
|
|
210
|
+
)
|
|
211
|
+
values ('storage.bucket', 'objects', repeat('1', 64))
|
|
212
|
+
on conflict (artifact_key) do update
|
|
213
|
+
set table_name = excluded.table_name,
|
|
214
|
+
content_hash = excluded.content_hash,
|
|
215
|
+
updated_at = now();`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
219
|
+
} finally {
|
|
220
|
+
await cleanupIntegrationProject(appRoot, minikubeRoot);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
INTEGRATION_TIMEOUT_MS,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
integrationTest(
|
|
227
|
+
'status reports clean disabled profile as skipped',
|
|
228
|
+
async () => {
|
|
229
|
+
const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await writeGeneratedFiles(
|
|
233
|
+
appRoot,
|
|
234
|
+
generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
|
|
235
|
+
namespaceHint,
|
|
236
|
+
}).files,
|
|
237
|
+
);
|
|
238
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
239
|
+
|
|
240
|
+
const output = await runGeneratedStatus(minikubeRoot);
|
|
241
|
+
expect(output).toContain('- profile reconciliation: skipped (no profile table configured)');
|
|
242
|
+
expect(output).toContain('- profile schema: skipped (no profile table configured)');
|
|
243
|
+
} finally {
|
|
244
|
+
await cleanupIntegrationProject(appRoot, minikubeRoot);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
INTEGRATION_TIMEOUT_MS,
|
|
248
|
+
);
|
|
249
|
+
|
|
135
250
|
integrationTest(
|
|
136
251
|
'rejects profile table rename once generated state exists',
|
|
137
252
|
async () => {
|
|
@@ -789,6 +904,59 @@ exit 0
|
|
|
789
904
|
throw new Error('Expected generated status script to fail');
|
|
790
905
|
}
|
|
791
906
|
|
|
907
|
+
async function runGeneratedStatus(minikubeRoot: string): Promise<string> {
|
|
908
|
+
const fakeBin = path.join(minikubeRoot, 'fake-status-bin');
|
|
909
|
+
const fakeKubectl = path.join(fakeBin, 'kubectl');
|
|
910
|
+
await mkdir(fakeBin, { recursive: true });
|
|
911
|
+
await writeFile(
|
|
912
|
+
fakeKubectl,
|
|
913
|
+
`#!/usr/bin/env bash
|
|
914
|
+
set -euo pipefail
|
|
915
|
+
|
|
916
|
+
if [[ "\${1:-}" == "config" && "\${2:-}" == "current-context" ]]; then
|
|
917
|
+
echo fake-context
|
|
918
|
+
exit 0
|
|
919
|
+
fi
|
|
920
|
+
|
|
921
|
+
if [[ "\${1:-}" == "cluster-info" ]]; then
|
|
922
|
+
echo "Kubernetes control plane is running"
|
|
923
|
+
exit 0
|
|
924
|
+
fi
|
|
925
|
+
|
|
926
|
+
if [[ "\${1:-}" == "get" && "\${2:-}" == "namespace" ]]; then
|
|
927
|
+
exit 0
|
|
928
|
+
fi
|
|
929
|
+
|
|
930
|
+
if [[ "\${1:-}" == "get" && "\${2:-}" == "all" ]]; then
|
|
931
|
+
exit 0
|
|
932
|
+
fi
|
|
933
|
+
|
|
934
|
+
if [[ "\${1:-}" == "-n" && "\${3:-}" == "get" && "\${4:-}" == "configmap" ]]; then
|
|
935
|
+
exit 0
|
|
936
|
+
fi
|
|
937
|
+
|
|
938
|
+
if [[ "\${1:-}" == "-n" && "\${3:-}" == "get" && "\${4:-}" == "secret" ]]; then
|
|
939
|
+
exit 1
|
|
940
|
+
fi
|
|
941
|
+
|
|
942
|
+
exit 0
|
|
943
|
+
`,
|
|
944
|
+
'utf8',
|
|
945
|
+
);
|
|
946
|
+
await chmod(fakeKubectl, 0o755);
|
|
947
|
+
|
|
948
|
+
const result = await execFile(path.join(minikubeRoot, 'scripts/status.sh'), {
|
|
949
|
+
cwd: minikubeRoot,
|
|
950
|
+
env: {
|
|
951
|
+
...process.env,
|
|
952
|
+
PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
|
|
953
|
+
SUPABASE_PROJECT_DIR: minikubeRoot,
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
958
|
+
}
|
|
959
|
+
|
|
792
960
|
async function runSupabaseSql(minikubeRoot: string, sql: string): Promise<void> {
|
|
793
961
|
const { stdout } = await execFile('supabase', ['--workdir', minikubeRoot, 'status', '-o', 'env']);
|
|
794
962
|
const dbUrl = readStatusEnv(stdout, 'DB_URL');
|
|
@@ -302,6 +302,57 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
|
|
|
302
302
|
expect(status).toContain('generated trigger function execute privilege must be revoked');
|
|
303
303
|
});
|
|
304
304
|
|
|
305
|
+
test('generates relation-safe disabled profile verification for local env and status', () => {
|
|
306
|
+
const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
|
|
307
|
+
const status = getGeneratedFileContent('chess', 'infra/minikube/scripts/status.sh');
|
|
308
|
+
const expectedDynamicLookup = `execute
|
|
309
|
+
'select exists (
|
|
310
|
+
select 1
|
|
311
|
+
from ankhorage_internal.generated_schema_state
|
|
312
|
+
where artifact_key = $1
|
|
313
|
+
)'
|
|
314
|
+
into has_profile_state
|
|
315
|
+
using 'auth.profile';`;
|
|
316
|
+
|
|
317
|
+
for (const generated of [script, status]) {
|
|
318
|
+
expect(generated).toContain('has_profile_state boolean := false;');
|
|
319
|
+
expect(generated).toContain(expectedDynamicLookup);
|
|
320
|
+
expect(generated).toContain(
|
|
321
|
+
'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration',
|
|
322
|
+
);
|
|
323
|
+
expect(generated).not.toContain(
|
|
324
|
+
`if to_regclass('ankhorage_internal.generated_schema_state') is not null
|
|
325
|
+
and exists (`,
|
|
326
|
+
);
|
|
327
|
+
expect(generated.replace(expectedDynamicLookup, '')).not.toContain(
|
|
328
|
+
`from ankhorage_internal.generated_schema_state
|
|
329
|
+
where artifact_key = 'auth.profile'`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
expect(script).toContain('Generated profile disabled-state verification');
|
|
334
|
+
expect(status).toContain('- profile reconciliation: skipped (no profile table configured)');
|
|
335
|
+
expect(status).toContain('- profile schema: skipped (no profile table configured)');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('status succeeds for a clean disabled profile local Supabase stack', () => {
|
|
339
|
+
const run = runGeneratedStatusScript({
|
|
340
|
+
manifest: createInfraManifest(),
|
|
341
|
+
supabaseProjectId: 'plain',
|
|
342
|
+
env: {
|
|
343
|
+
AUTH_RUNTIME_MODE: 'local',
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(run.status).toBe(0);
|
|
348
|
+
expect(run.combinedOutput).toContain('- immutable migrations: applied');
|
|
349
|
+
expect(run.combinedOutput).toContain(
|
|
350
|
+
'- profile reconciliation: skipped (no profile table configured)',
|
|
351
|
+
);
|
|
352
|
+
expect(run.combinedOutput).toContain('- profile schema: skipped (no profile table configured)');
|
|
353
|
+
expect(run.supabaseLog).toContain('--workdir');
|
|
354
|
+
});
|
|
355
|
+
|
|
305
356
|
test('does not reject SUPABASE_PROJECT_ID for non-Supabase status checks', () => {
|
|
306
357
|
const run = runGeneratedStatusScript({
|
|
307
358
|
manifest: {
|
|
@@ -546,16 +597,24 @@ function runGeneratedStatusScript(args: {
|
|
|
546
597
|
const tempRoot = mkdtempSync(join(tmpdir(), 'infra-status-'));
|
|
547
598
|
const root = join(tempRoot, 'infra', 'minikube');
|
|
548
599
|
const scriptsDir = join(root, 'scripts');
|
|
600
|
+
const supabaseDir = join(root, 'supabase');
|
|
549
601
|
const binDir = join(tempRoot, 'bin');
|
|
550
602
|
const scriptPath = join(scriptsDir, 'status.sh');
|
|
551
603
|
const kubectlLogPath = join(tempRoot, 'kubectl.log');
|
|
552
604
|
const supabaseLogPath = join(tempRoot, 'supabase.log');
|
|
605
|
+
const psqlLogPath = join(tempRoot, 'psql.log');
|
|
553
606
|
|
|
554
607
|
mkdirSync(scriptsDir, { recursive: true });
|
|
555
608
|
mkdirSync(binDir, { recursive: true });
|
|
556
609
|
writeFileSync(join(root, '.env.example'), '');
|
|
557
610
|
writeFileSync(kubectlLogPath, '');
|
|
558
611
|
writeFileSync(supabaseLogPath, '');
|
|
612
|
+
writeFileSync(psqlLogPath, '');
|
|
613
|
+
|
|
614
|
+
if (args.supabaseProjectId !== null) {
|
|
615
|
+
mkdirSync(supabaseDir, { recursive: true });
|
|
616
|
+
writeFileSync(join(supabaseDir, 'config.toml'), `project_id = "${args.supabaseProjectId}"\n`);
|
|
617
|
+
}
|
|
559
618
|
|
|
560
619
|
const artifact = generateMinikubeBaseArtifacts({
|
|
561
620
|
manifest: args.manifest,
|
|
@@ -626,12 +685,48 @@ if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]];
|
|
|
626
685
|
exit 0
|
|
627
686
|
fi
|
|
628
687
|
|
|
688
|
+
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
|
|
689
|
+
cat <<'ENV'
|
|
690
|
+
DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
691
|
+
API_URL=http://127.0.0.1:54321
|
|
692
|
+
ANON_KEY=anon
|
|
693
|
+
SERVICE_ROLE_KEY=service
|
|
694
|
+
JWT_SECRET=secret
|
|
695
|
+
ENV
|
|
696
|
+
exit 0
|
|
697
|
+
fi
|
|
698
|
+
|
|
699
|
+
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
|
|
700
|
+
exit 0
|
|
701
|
+
fi
|
|
702
|
+
|
|
629
703
|
echo "unexpected supabase call: $*" >&2
|
|
630
704
|
exit 1
|
|
631
705
|
`,
|
|
632
706
|
);
|
|
633
707
|
chmodSync(supabaseStubPath, 0o755);
|
|
634
708
|
|
|
709
|
+
const psqlStubPath = join(binDir, 'psql');
|
|
710
|
+
writeFileSync(
|
|
711
|
+
psqlStubPath,
|
|
712
|
+
`#!/bin/bash
|
|
713
|
+
set -euo pipefail
|
|
714
|
+
printf '%s\\n' "$*" >> "\${PSQL_STUB_LOG}"
|
|
715
|
+
|
|
716
|
+
if [[ "$*" == *"select version from supabase_migrations.schema_migrations"* ]]; then
|
|
717
|
+
exit 0
|
|
718
|
+
fi
|
|
719
|
+
|
|
720
|
+
if [[ "$*" == *"-f"* ]]; then
|
|
721
|
+
exit 0
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
echo "unexpected psql call: $*" >&2
|
|
725
|
+
exit 1
|
|
726
|
+
`,
|
|
727
|
+
);
|
|
728
|
+
chmodSync(psqlStubPath, 0o755);
|
|
729
|
+
|
|
635
730
|
const result = spawnSync('/bin/bash', [scriptPath], {
|
|
636
731
|
cwd: tempRoot,
|
|
637
732
|
env: {
|
|
@@ -639,6 +734,7 @@ exit 1
|
|
|
639
734
|
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
|
640
735
|
KUBECTL_STUB_LOG: kubectlLogPath,
|
|
641
736
|
SUPABASE_STUB_LOG: supabaseLogPath,
|
|
737
|
+
PSQL_STUB_LOG: psqlLogPath,
|
|
642
738
|
...args.env,
|
|
643
739
|
},
|
|
644
740
|
encoding: 'utf-8',
|
|
@@ -1067,13 +1067,21 @@ $$;`;
|
|
|
1067
1067
|
|
|
1068
1068
|
function getSupabaseProfileDisabledGuardSql(): string {
|
|
1069
1069
|
return `do $$
|
|
1070
|
+
declare
|
|
1071
|
+
has_profile_state boolean := false;
|
|
1070
1072
|
begin
|
|
1071
|
-
if to_regclass('ankhorage_internal.generated_schema_state') is not null
|
|
1072
|
-
|
|
1073
|
-
select
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1073
|
+
if to_regclass('ankhorage_internal.generated_schema_state') is not null then
|
|
1074
|
+
execute
|
|
1075
|
+
'select exists (
|
|
1076
|
+
select 1
|
|
1077
|
+
from ankhorage_internal.generated_schema_state
|
|
1078
|
+
where artifact_key = $1
|
|
1079
|
+
)'
|
|
1080
|
+
into has_profile_state
|
|
1081
|
+
using 'auth.profile';
|
|
1082
|
+
end if;
|
|
1083
|
+
|
|
1084
|
+
if has_profile_state then
|
|
1077
1085
|
raise exception 'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration';
|
|
1078
1086
|
end if;
|
|
1079
1087
|
end;
|