@ankhorage/infra 0.4.2 → 1.0.1

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -1
  4. package/dist/adapters/minikube/auth/supabase/index.js +14 -39
  5. package/dist/adapters/minikube/auth/supabase/index.js.map +1 -1
  6. package/dist/adapters/minikube/auth/supabase/profile.d.ts +0 -1
  7. package/dist/adapters/minikube/auth/supabase/profile.d.ts.map +1 -1
  8. package/dist/adapters/minikube/auth/supabase/profile.js +1 -1
  9. package/dist/adapters/minikube/auth/supabase/profile.js.map +1 -1
  10. package/dist/adapters/minikube/authz/cerbos/index.d.ts.map +1 -1
  11. package/dist/adapters/minikube/authz/cerbos/index.js +22 -2
  12. package/dist/adapters/minikube/authz/cerbos/index.js.map +1 -1
  13. package/dist/adapters/minikube/base/index.d.ts +5 -2
  14. package/dist/adapters/minikube/base/index.d.ts.map +1 -1
  15. package/dist/adapters/minikube/base/index.js +2764 -1841
  16. package/dist/adapters/minikube/base/index.js.map +1 -1
  17. package/dist/adapters/minikube/contracts.d.ts +25 -0
  18. package/dist/adapters/minikube/contracts.d.ts.map +1 -1
  19. package/dist/adapters/minikube/contracts.js +1 -0
  20. package/dist/adapters/minikube/contracts.js.map +1 -1
  21. package/dist/adapters/minikube/index.d.ts.map +1 -1
  22. package/dist/adapters/minikube/index.js +18 -33
  23. package/dist/adapters/minikube/index.js.map +1 -1
  24. package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts.map +1 -1
  25. package/dist/adapters/minikube/secrets/supabase-vault/index.js +49 -6
  26. package/dist/adapters/minikube/secrets/supabase-vault/index.js.map +1 -1
  27. package/dist/adapters/minikube/storage/supabase/index.d.ts.map +1 -1
  28. package/dist/adapters/minikube/storage/supabase/index.js +2 -1
  29. package/dist/adapters/minikube/storage/supabase/index.js.map +1 -1
  30. package/package.json +4 -1
  31. package/src/adapters/minikube/README.md +46 -38
  32. package/src/adapters/minikube/auth/supabase/index.ts +14 -40
  33. package/src/adapters/minikube/auth/supabase/profile.ts +1 -1
  34. package/src/adapters/minikube/authz/cerbos/index.ts +25 -3
  35. package/src/adapters/minikube/base/index.test.ts +399 -673
  36. package/src/adapters/minikube/base/index.ts +2969 -1874
  37. package/src/adapters/minikube/base/twoAppIsolation.integration.test.ts +636 -0
  38. package/src/adapters/minikube/contracts.ts +29 -0
  39. package/src/adapters/minikube/index.ts +18 -44
  40. package/src/adapters/minikube/secrets/index.test.ts +15 -7
  41. package/src/adapters/minikube/secrets/supabase-vault/index.ts +53 -6
  42. package/src/adapters/minikube/storage/supabase/index.ts +1 -5
  43. package/src/apis.test.ts +2 -1
  44. package/src/index.test.ts +64 -577
  45. package/src/minikubeImageLifecycle.test.ts +4 -3
package/src/index.test.ts CHANGED
@@ -5,7 +5,7 @@ import { generateInfrastructure } from './index';
5
5
  import type { InfraManifestInput } from './types';
6
6
 
7
7
  describe('generateInfrastructure', () => {
8
- test('generates minikube artifacts with supabase auth and cerbos authz resources', () => {
8
+ test('generates minikube artifacts for app-owned Kubernetes infrastructure', () => {
9
9
  const manifest: InfraManifestInput = {
10
10
  deployment: {
11
11
  target: 'minikube',
@@ -46,267 +46,62 @@ describe('generateInfrastructure', () => {
46
46
  };
47
47
 
48
48
  const result = generateInfrastructure(manifest, { appManifest: createAppManifest('alpha') });
49
- const paths = result.files.map((f) => f.path);
49
+ const paths = result.files.map((file) => file.path);
50
50
 
51
51
  expect(result.meta.target).toBe('minikube');
52
52
  expect(result.meta.providers).toEqual(['auto', 'cerbos', 'supabase']);
53
- expect(paths).toContain('infra/minikube/k8s/namespace.yaml');
53
+ expect(paths).toContain('infra/minikube/k8s/namespaces/app.yaml');
54
+ expect(paths).toContain('infra/minikube/k8s/namespaces/supabase.yaml');
54
55
  expect(paths).toContain('infra/minikube/k8s/app/deployment.yaml');
55
56
  expect(paths).toContain('infra/minikube/k8s/app/service.yaml');
57
+ expect(paths).toContain('infra/minikube/k8s/supabase/postgres.yaml');
58
+ expect(paths).toContain('infra/minikube/k8s/supabase/gateway.yaml');
56
59
  expect(paths).toContain('infra/minikube/k8s/auth/supabase/supabase-auth.configmap.yaml');
57
60
  expect(paths).toContain('infra/minikube/k8s/auth/supabase/app-runtime-auth.env.configmap.yaml');
58
61
  expect(paths).toContain('infra/minikube/k8s/authz/cerbos/cerbos.policy.configmap.yaml');
59
62
  expect(paths).toContain('infra/minikube/k8s/authz/cerbos/cerbos.deployment.yaml');
60
- expect(paths).toContain('infra/minikube/auth/supabase-runtime-wiring.md');
61
63
  expect(paths).toContain('infra/minikube/k8s/storage/supabase/supabase-storage.configmap.yaml');
62
64
  expect(paths).toContain(
63
65
  'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
64
66
  );
65
67
  expect(paths).toContain('infra/minikube/scripts/up.sh');
66
- expect(paths).toContain('infra/minikube/scripts/build-app-image.sh');
68
+ expect(paths).toContain('infra/minikube/scripts/down.sh');
69
+ expect(paths).toContain('infra/minikube/scripts/reset.sh');
70
+ expect(paths).toContain('infra/minikube/scripts/destroy.sh');
71
+ expect(paths).toContain('infra/minikube/scripts/status.sh');
67
72
  expect(paths).toContain('infra/minikube/scripts/port-forward.sh');
68
- expect(paths).toContain('infra/minikube/scripts/supabase-local-env.sh');
69
- expect(paths).toContain('infra/minikube/app-image/Dockerfile');
70
- expect(result.warnings).toContain(
71
- 'Storage buckets are configured but not created automatically yet. Ensure buckets exist in Supabase Storage: assets.',
72
- );
73
-
74
- const namespaceFile = result.files.find((f) => f.path === 'infra/minikube/k8s/namespace.yaml');
75
- expect(namespaceFile?.content).toContain('name: alpha-local');
76
-
77
- const appInfraConfigMap = result.files.find(
78
- (f) => f.path === 'infra/minikube/k8s/app.configmap.yaml',
79
- );
80
- expect(appInfraConfigMap?.content).toContain('STORAGE_PROVIDER: "supabase"');
81
- expect(appInfraConfigMap?.content).toContain('STORAGE_BUCKETS: "assets"');
82
- expect(appInfraConfigMap?.content).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
83
-
84
- const upScript = result.files.find((f) => f.path === 'infra/minikube/scripts/up.sh');
85
- expect(upScript?.executable).toBe(true);
86
- expect(upScript?.content).toContain('NAMESPACE="${ANKH_NAMESPACE:-alpha-local}"');
87
- expect(upScript?.content).toContain('APP_BUILD_ENABLED="${APP_BUILD_ENABLED:-true}"');
88
- expect(upScript?.content).toContain('BUILD_SCRIPT="${SCRIPT_DIR}/build-app-image.sh"');
89
- expect(upScript?.content).toContain('APP_IMAGE="${APP_IMAGE:-ankh/alpha-local:dev}"');
90
- expect(upScript?.content).toContain(
91
- 'APP_FORCE_ROLLOUT_RESTART="${APP_FORCE_ROLLOUT_RESTART:-true}"',
92
- );
93
- expect(upScript?.content).toContain('"${BUILD_SCRIPT}"');
94
- expect(upScript?.content).toContain(
95
- 'minikube -p "${PROFILE}" image load --daemon=true --overwrite=true "${APP_IMAGE}"',
96
- );
97
- expect(upScript?.content).toContain(
98
- 'APP_IMAGE_SYNC_STRATEGY="${APP_IMAGE_SYNC_STRATEGY:-docker-load}"',
99
- );
100
- expect(upScript?.content).toContain('AUTH_RUNTIME_MODE="${AUTH_RUNTIME_MODE:-local}"');
101
- expect(upScript?.content).toContain(
102
- 'minikube -p "${PROFILE}" image build -t "${APP_IMAGE}" -f "${ROOT_DIR}/app-image/Dockerfile" "${EXPORT_DIR}"',
103
- );
104
- expect(upScript?.content).toContain('APP_IMAGE_PULL_SECRET_NAME');
105
- expect(upScript?.content).toContain('create secret docker-registry');
106
- expect(upScript?.content).toContain('PULL_SECRET_PATCH=');
107
- expect(upScript?.content).toContain('--patch "${PULL_SECRET_PATCH}"');
108
- expect(upScript?.content).toContain(
109
- 'SUPABASE_SECRET_SYNC_ENABLED="${SUPABASE_SECRET_SYNC_ENABLED:-true}"',
110
- );
111
- expect(upScript?.content).toContain(
112
- 'SUPABASE_LOCAL_ENV_SCRIPT="${SCRIPT_DIR}/supabase-local-env.sh"',
113
- );
114
- expect(upScript?.content).toContain(
115
- 'Supabase keys missing; running ${SUPABASE_LOCAL_ENV_SCRIPT}.',
116
- );
117
- expect(upScript?.content).toContain('create secret generic supabase-auth-secrets');
118
- expect(upScript?.content).toContain(
119
- 'Synchronized supabase-auth-secrets from environment values.',
120
- );
121
- expect(upScript?.content).toContain('imagePullSecrets');
122
- expect(upScript?.content).toContain('set image deployment/app-runtime');
123
- expect(upScript?.content).toContain('scale deployment/app-runtime');
124
- expect(upScript?.content).toContain('rollout restart deployment/app-runtime');
125
- expect(upScript?.content).toContain('rollout status deployment/app-runtime --timeout=180s');
126
-
127
- const kustomization = result.files.find(
128
- (f) => f.path === 'infra/minikube/k8s/kustomization.yaml',
129
- );
130
- expect(kustomization?.content).toContain('auth/supabase/supabase-auth.configmap.yaml');
131
- expect(kustomization?.content).toContain('auth/supabase/app-runtime-auth.env.configmap.yaml');
132
- expect(kustomization?.content).toContain('authz/cerbos/cerbos.policy.configmap.yaml');
133
- expect(kustomization?.content).toContain('authz/cerbos/cerbos.deployment.yaml');
134
- expect(kustomization?.content).toContain('storage/supabase/supabase-storage.configmap.yaml');
135
- expect(kustomization?.content).toContain(
136
- 'storage/supabase/app-runtime-storage.env.configmap.yaml',
137
- );
138
- expect(kustomization?.content).toContain('app/deployment.yaml');
139
- expect(kustomization?.content).toContain('app/service.yaml');
140
-
141
- const envExample = result.files.find((f) => f.path === 'infra/minikube/.env.example');
142
- expect(envExample?.content).toContain('SUPABASE_JWT_SECRET=');
143
- expect(envExample?.content).toContain('SUPABASE_SECRET_SYNC_ENABLED=true');
144
- expect(envExample?.content).toContain('CERBOS_URL=http://cerbos:3592');
145
- expect(envExample?.content).toContain('EXPO_PUBLIC_SUPABASE_URL=');
146
- expect(envExample?.content).toContain('APP_BUILD_ENABLED=true');
147
- expect(envExample?.content).toContain('APP_SOURCE_DIR=');
148
- expect(envExample?.content).toContain('APP_WEB_EXPORT_DIR=.ankh/web-export');
149
- expect(envExample?.content).toContain('SUPABASE_PROJECT_DIR=');
150
- expect(envExample?.content).toContain('SUPABASE_LOCAL_PORT_BASE=55540');
151
- expect(envExample?.content).toContain('SUPABASE_LOCAL_ANALYTICS_PORT=55545');
152
- expect(envExample?.content).toContain('APP_IMAGE=ankh/alpha-local:dev');
153
- expect(envExample?.content).toContain('APP_IMAGE_SYNC_STRATEGY=docker-load');
154
- expect(envExample?.content).toContain('AUTH_RUNTIME_MODE=local');
155
- expect(envExample?.content).toContain('APP_PORT_FORWARD_LOCAL_PORT=18080');
156
- expect(envExample?.content).toContain('APP_PORT_FORWARD_REMOTE_PORT=80');
157
- expect(envExample?.content).toContain('APP_REPLICAS=');
158
- expect(envExample?.content).toContain('APP_FORCE_ROLLOUT_RESTART=true');
159
- expect(envExample?.content).toContain('APP_IMAGE_PULL_SECRET_NAME=');
160
- expect(envExample?.content).toContain('APP_IMAGE_PULL_SECRET_SERVER=');
161
- expect(envExample?.content).toContain('STORAGE_PROVIDER=supabase');
162
- expect(envExample?.content).toContain('STORAGE_RUNTIME_MODE=local');
163
- expect(envExample?.content).toContain('STORAGE_BUCKETS=assets');
164
- expect(envExample?.content).toContain('STORAGE_DEFAULT_BUCKET=assets');
165
- expect(envExample?.content).toContain('EXPO_PUBLIC_STORAGE_PROVIDER=supabase');
166
- expect(envExample?.content).toContain('EXPO_PUBLIC_STORAGE_BUCKETS=assets');
167
- expect(envExample?.content).toContain('EXPO_PUBLIC_STORAGE_DEFAULT_BUCKET=assets');
168
-
169
- const minikubeReadme = result.files.find((f) => f.path === 'infra/minikube/README.md');
170
- expect(minikubeReadme?.content).toContain('SUPABASE_LOCAL_ANALYTICS_PORT');
171
- expect(minikubeReadme?.content).toContain('55545');
172
- expect(minikubeReadme?.content).toContain('Supabase local project identity: `alpha`');
173
- expect(minikubeReadme?.content).toContain('$APP_SOURCE_DIR/.env.local');
174
- expect(minikubeReadme?.content).toContain('apps/card/.env.local');
175
-
176
- const runtimeGuide = result.files.find(
177
- (f) => f.path === 'infra/minikube/auth/supabase-runtime-wiring.md',
178
- );
179
- expect(runtimeGuide?.content).toContain('envFrom:');
180
- expect(runtimeGuide?.content).toContain('supabase-auth-secrets');
181
- expect(runtimeGuide?.content).toContain('AUTH_SIGN_IN_IDENTIFIERS');
182
- expect(runtimeGuide?.content).toContain('AUTH_SIGN_UP_REQUIRED_FIELDS');
183
- expect(runtimeGuide?.content).toContain('AUTH_SIGN_UP_OPTIONAL_FIELDS');
184
- expect(runtimeGuide?.content).toContain('AUTH_SIGN_UP_POLICY');
185
- expect(runtimeGuide?.content).toContain('AUTH_PROFILE_FIELDS');
186
-
187
- const supabaseConfigMap = result.files.find(
188
- (f) => f.path === 'infra/minikube/k8s/auth/supabase/supabase-auth.configmap.yaml',
189
- );
190
- expect(supabaseConfigMap?.content).toContain('AUTH_SIGN_IN_IDENTIFIERS: "email,username"');
191
- expect(supabaseConfigMap?.content).toContain('AUTH_SIGN_UP_REQUIRED_FIELDS: "email,password"');
192
- expect(supabaseConfigMap?.content).toContain(
193
- 'AUTH_SIGN_UP_OPTIONAL_FIELDS: "firstName,lastName"',
194
- );
195
- expect(supabaseConfigMap?.content).toContain('AUTH_SIGN_UP_POLICY: "requireVerification"');
196
- expect(supabaseConfigMap?.content).toContain(
197
- 'AUTH_PROFILE_FIELDS: "email,firstName,lastName,avatarUrl"',
198
- );
199
-
200
- const runtimeAuthConfigMap = result.files.find(
201
- (f) => f.path === 'infra/minikube/k8s/auth/supabase/app-runtime-auth.env.configmap.yaml',
202
- );
203
- expect(runtimeAuthConfigMap?.content).toContain('AUTH_SIGN_IN_IDENTIFIERS: "email,username"');
204
- expect(runtimeAuthConfigMap?.content).toContain(
205
- 'AUTH_SIGN_UP_REQUIRED_FIELDS: "email,password"',
206
- );
207
- expect(runtimeAuthConfigMap?.content).toContain(
208
- 'AUTH_SIGN_UP_OPTIONAL_FIELDS: "firstName,lastName"',
209
- );
210
- expect(runtimeAuthConfigMap?.content).toContain('AUTH_SIGN_UP_POLICY: "requireVerification"');
211
- expect(runtimeAuthConfigMap?.content).toContain(
212
- 'AUTH_PROFILE_FIELDS: "email,firstName,lastName,avatarUrl"',
213
- );
214
-
215
- const appDeployment = result.files.find(
216
- (f) => f.path === 'infra/minikube/k8s/app/deployment.yaml',
217
- );
218
- expect(appDeployment?.content).toContain('image: ankh/alpha-local:dev');
219
- expect(appDeployment?.content).toContain('name: app-infra-config');
220
- expect(appDeployment?.content).toContain('name: app-runtime-auth-env');
221
- expect(appDeployment?.content).toContain('name: app-runtime-storage-env');
222
- expect(appDeployment?.content).toContain('name: supabase-auth-secrets');
223
-
224
- const appDeploymentContent = appDeployment?.content ?? '';
225
- const appInfraIndex = appDeploymentContent.indexOf('name: app-infra-config');
226
- const authIndex = appDeploymentContent.indexOf('name: app-runtime-auth-env');
227
- const storageIndex = appDeploymentContent.indexOf('name: app-runtime-storage-env');
228
- const supabaseSecretsIndex = appDeploymentContent.indexOf('name: supabase-auth-secrets');
229
-
230
- // envFrom precedence: later sources win on duplicate keys; explicit `env` entries win over `envFrom`.
231
- // Keep storage discovery (`app-runtime-storage-env`) before provider secrets to allow future overlap.
232
- expect(appInfraIndex).toBeGreaterThanOrEqual(0);
233
- expect(authIndex).toBeGreaterThan(appInfraIndex);
234
- expect(storageIndex).toBeGreaterThan(authIndex);
235
- expect(supabaseSecretsIndex).toBeGreaterThan(storageIndex);
236
-
237
- const appBuildScript = result.files.find(
238
- (f) => f.path === 'infra/minikube/scripts/build-app-image.sh',
239
- );
240
- expect(appBuildScript?.executable).toBe(true);
241
- expect(appBuildScript?.content).toContain('bunx expo export --platform web --output-dir');
242
- expect(appBuildScript?.content).toContain('docker build -t "${APP_IMAGE}"');
243
- expect(appBuildScript?.content).toContain('APP_SOURCE_DIR=');
244
-
245
- const portForwardScript = result.files.find(
246
- (f) => f.path === 'infra/minikube/scripts/port-forward.sh',
247
- );
248
- expect(portForwardScript?.executable).toBe(true);
249
- expect(portForwardScript?.content).toContain(
250
- 'kubectl -n "${NAMESPACE}" port-forward service/app-runtime "${LOCAL_PORT}:${REMOTE_PORT}"',
251
- );
252
- expect(portForwardScript?.content).toContain(
253
- 'LOCAL_PORT="${APP_PORT_FORWARD_LOCAL_PORT:-18080}"',
254
- );
255
-
256
- const supabaseLocalEnvScript = result.files.find(
257
- (f) => f.path === 'infra/minikube/scripts/supabase-local-env.sh',
258
- );
259
- expect(supabaseLocalEnvScript?.executable).toBe(true);
260
- expect(supabaseLocalEnvScript?.content).toContain('supabase status -o env');
261
- expect(supabaseLocalEnvScript?.content).toContain(
262
- 'supabase --workdir "${SUPABASE_PROJECT_DIR}" init --yes',
263
- );
264
- expect(supabaseLocalEnvScript?.content).toContain(
265
- 'supabase --workdir "${SUPABASE_PROJECT_DIR}" start',
266
- );
267
- expect(supabaseLocalEnvScript?.content).toContain(
268
- 'supabase --workdir "${SUPABASE_PROJECT_DIR}" migration up --local',
269
- );
270
- expect(supabaseLocalEnvScript?.content).toContain('SUPABASE_PROJECT_DIR');
271
- expect(supabaseLocalEnvScript?.content).toContain(
272
- 'SUPABASE_PROJECT_DIR="${SUPABASE_PROJECT_DIR:-${ROOT_DIR}}"',
273
- );
274
- expect(supabaseLocalEnvScript?.content).toContain('supabase CLI >= 2.106.0 is required');
275
- expect(supabaseLocalEnvScript?.content).toContain('SUPABASE_LOCAL_ANALYTICS_PORT');
276
- expect(supabaseLocalEnvScript?.content).toContain('export SUPABASE_LOCAL_ANALYTICS_PORT');
277
- expect(supabaseLocalEnvScript?.content).toContain(
278
- 'Patch("analytics", "port", _env_required("SUPABASE_LOCAL_ANALYTICS_PORT"))',
279
- );
280
- expect(supabaseLocalEnvScript?.content).toContain(
281
- 'upsert_env "${ENV_FILE}" "SUPABASE_LOCAL_ANALYTICS_PORT" "${SUPABASE_LOCAL_ANALYTICS_PORT}"',
282
- );
283
-
284
- const appImageDockerfile = result.files.find(
285
- (f) => f.path === 'infra/minikube/app-image/Dockerfile',
286
- );
287
- expect(appImageDockerfile?.content).toContain('FROM nginx:1.27-alpine');
288
- expect(appImageDockerfile?.content).toContain('listen 8080;');
289
- expect(appImageDockerfile?.content).toContain('COPY . /usr/share/nginx/html');
290
-
291
- const cerbosConfig = result.files.find(
292
- (f) => f.path === 'infra/minikube/k8s/authz/cerbos/cerbos.configmap.yaml',
293
- );
294
- const cerbosPolicy = result.files.find(
295
- (f) => f.path === 'infra/minikube/k8s/authz/cerbos/cerbos.policy.configmap.yaml',
296
- );
297
- const cerbosDeployment = result.files.find(
298
- (f) => f.path === 'infra/minikube/k8s/authz/cerbos/cerbos.deployment.yaml',
299
- );
300
-
301
- expect(cerbosConfig?.content).toContain('name: cerbos-config');
302
- expect(cerbosConfig?.content).toContain('config.yaml: |');
303
- expect(cerbosConfig?.content).not.toContain('resource_policy.yaml');
304
- expect(cerbosPolicy?.content).toContain('name: cerbos-policy');
305
- expect(cerbosPolicy?.content).toContain('app.resource_policy.yaml: |');
306
- expect(cerbosPolicy?.content).not.toContain('route.resource_policy.yaml: |');
307
- expect(cerbosPolicy?.content).not.toContain('screen.resource_policy.yaml: |');
308
- expect(cerbosPolicy?.content).not.toContain('metadata.yaml');
309
- expect(cerbosDeployment?.content).toContain('name: cerbos-policy');
73
+ expect(paths).not.toContain('infra/minikube/scripts/supabase-local-env.sh');
74
+
75
+ const envExample = getFile(result.files, 'infra/minikube/.env.example');
76
+ expect(envExample).toContain('ANKH_APP_SLUG=alpha');
77
+ expect(envExample).not.toContain('SUPABASE_RUNTIME_ENABLED=');
78
+ expect(envExample).toContain('EXPO_PUBLIC_SUPABASE_URL=');
79
+ expect(envExample).toContain('APP_IMAGE=ankh/alpha:dev');
80
+ expect(envExample).not.toContain('MINIKUBE_PROFILE=');
81
+ expect(envExample).not.toContain('SUPABASE_PROJECT_DIR=');
82
+
83
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
84
+ expect(upScript).toContain('SUPABASE_RUNTIME_ENABLED="true"');
85
+ expect(upScript).not.toContain('SUPABASE_RUNTIME_ENABLED="${SUPABASE_RUNTIME_ENABLED');
86
+ expect(upScript).toContain('minikube start -p "${PROFILE}"');
87
+ expect(upScript).toContain('kubectl --context "${PROFILE}" apply -k "${K8S_DIR}"');
88
+ expect(upScript).toContain('supabase migration up --db-url "${SUPABASE_DB_URL}"');
89
+ expect(upScript).toContain('create secret generic supabase-runtime-secrets');
90
+ expect(upScript).toContain('create secret generic supabase-public-runtime');
91
+ expect(upScript).toContain('rollout status deployment/app-runtime --timeout=180s');
92
+ expect(upScript).not.toContain('supabase start');
93
+ expect(upScript).not.toContain('supabase status');
94
+
95
+ const appDeployment = getFile(result.files, 'infra/minikube/k8s/app/deployment.yaml');
96
+ expect(appDeployment).toContain('namespace: app');
97
+ expect(appDeployment).toContain('image: ankh/alpha:dev');
98
+ expect(appDeployment).toContain('name: supabase-public-runtime');
99
+ expect(appDeployment).not.toContain('supabase-auth-secrets');
100
+
101
+ const runtimeGuide = getFile(result.files, 'infra/minikube/auth/supabase-runtime-wiring.md');
102
+ expect(runtimeGuide).toContain('supabase-public-runtime');
103
+ expect(runtimeGuide).toContain('${APP_SOURCE_DIR}/.env.local');
104
+ expect(runtimeGuide).not.toContain('SUPABASE_SERVICE_ROLE_KEY');
310
105
  });
311
106
 
312
107
  test('generates minikube storage artifacts for explicit supabase provider with normalized buckets', () => {
@@ -318,10 +113,6 @@ describe('generateInfrastructure', () => {
318
113
  auth: {
319
114
  scope: 'global',
320
115
  provider: 'supabase',
321
- authorization: {
322
- kind: 'ABAC',
323
- engine: 'cerbos',
324
- },
325
116
  },
326
117
  database: {
327
118
  provider: 'supabase',
@@ -335,7 +126,6 @@ describe('generateInfrastructure', () => {
335
126
  };
336
127
 
337
128
  const result = generateInfrastructure(manifest, {
338
- namespaceHint: 'shop',
339
129
  appManifest: createAppManifest('shop'),
340
130
  });
341
131
 
@@ -343,159 +133,13 @@ describe('generateInfrastructure', () => {
343
133
  'Storage buckets are configured but not created automatically yet. Ensure buckets exist in Supabase Storage: assets,uploads.',
344
134
  );
345
135
 
346
- const storageConfig = result.files.find(
347
- (f) => f.path === 'infra/minikube/k8s/storage/supabase/supabase-storage.configmap.yaml',
348
- );
349
- expect(storageConfig?.content).toContain('STORAGE_PROVIDER: "supabase"');
350
- expect(storageConfig?.content).toContain('STORAGE_BUCKETS: "assets,uploads"');
351
- expect(storageConfig?.content).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
352
-
353
- const runtimeStorageEnv = result.files.find(
354
- (f) =>
355
- f.path === 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
356
- );
357
- expect(runtimeStorageEnv?.content).toContain('STORAGE_RUNTIME_MODE: "local"');
358
- expect(runtimeStorageEnv?.content).toContain('STORAGE_BUCKETS: "assets,uploads"');
359
- expect(runtimeStorageEnv?.content).toContain('EXPO_PUBLIC_STORAGE_BUCKETS: "assets,uploads"');
360
- expect(runtimeStorageEnv?.content).toContain('EXPO_PUBLIC_STORAGE_DEFAULT_BUCKET: "assets"');
361
-
362
- const appInfraConfigMap = result.files.find(
363
- (f) => f.path === 'infra/minikube/k8s/app.configmap.yaml',
364
- );
365
- expect(appInfraConfigMap?.content).toContain('STORAGE_PROVIDER: "supabase"');
366
- expect(appInfraConfigMap?.content).toContain('STORAGE_BUCKETS: "assets,uploads"');
367
- expect(appInfraConfigMap?.content).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
368
- });
369
-
370
- test('resolves storage.provider="auto" to supabase when database provider is supabase', () => {
371
- const manifest: InfraManifestInput = {
372
- deployment: {
373
- target: 'minikube',
374
- monitoring: false,
375
- },
376
- database: {
377
- provider: 'supabase',
378
- tier: 'dev',
379
- },
380
- storage: {
381
- provider: 'auto',
382
- buckets: ['assets'],
383
- },
384
- plugins: [],
385
- };
386
-
387
- const result = generateInfrastructure(manifest, {
388
- namespaceHint: 'shop',
389
- appManifest: createAppManifest('shop'),
390
- });
391
-
392
- expect(result.files.map((f) => f.path)).toContain(
393
- 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
394
- );
395
-
396
- const envExample = result.files.find((f) => f.path === 'infra/minikube/.env.example');
397
- expect(envExample?.content).toContain('STORAGE_PROVIDER=supabase');
398
- expect(envExample?.content).toContain('EXPO_PUBLIC_STORAGE_PROVIDER=supabase');
399
- expect(envExample?.content).toContain('SUPABASE_URL=');
400
- expect(envExample?.content).toContain('EXPO_PUBLIC_SUPABASE_URL=');
401
-
402
- const appInfraConfigMap = result.files.find(
403
- (f) => f.path === 'infra/minikube/k8s/app.configmap.yaml',
404
- );
405
- expect(appInfraConfigMap?.content).toContain('STORAGE_PROVIDER: "supabase"');
406
- expect(appInfraConfigMap?.content).toContain('STORAGE_BUCKETS: "assets"');
407
- expect(appInfraConfigMap?.content).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
408
- });
409
-
410
- test('warns when explicit supabase provider buckets are empty after normalization', () => {
411
- const manifest: InfraManifestInput = {
412
- deployment: {
413
- target: 'minikube',
414
- monitoring: false,
415
- },
416
- auth: {
417
- scope: 'global',
418
- provider: 'supabase',
419
- authorization: {
420
- kind: 'ABAC',
421
- engine: 'cerbos',
422
- },
423
- },
424
- database: {
425
- provider: 'supabase',
426
- tier: 'dev',
427
- },
428
- storage: {
429
- provider: 'supabase',
430
- buckets: ['', ' ', ' '],
431
- },
432
- plugins: [],
433
- };
434
-
435
- const result = generateInfrastructure(manifest, {
436
- namespaceHint: 'shop',
437
- appManifest: createAppManifest('shop'),
438
- });
439
- expect(result.warnings).toContain(
440
- 'Storage buckets are empty after normalization; no storage artifacts generated.',
441
- );
442
- expect(result.files.map((f) => f.path)).not.toContain(
443
- 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
444
- );
445
- });
446
-
447
- test('warns when auto provider resolves to supabase but buckets are empty after normalization', () => {
448
- const manifest: InfraManifestInput = {
449
- deployment: {
450
- target: 'minikube',
451
- monitoring: false,
452
- },
453
- database: {
454
- provider: 'supabase',
455
- tier: 'dev',
456
- },
457
- storage: {
458
- provider: 'auto',
459
- buckets: [' ', ''],
460
- },
461
- plugins: [],
462
- };
463
-
464
- const result = generateInfrastructure(manifest, {
465
- namespaceHint: 'shop',
466
- appManifest: createAppManifest('shop'),
467
- });
468
- expect(result.warnings).toContain(
469
- 'Storage buckets are empty after normalization; no storage artifacts generated.',
470
- );
471
- expect(result.files.map((f) => f.path)).not.toContain(
472
- 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
473
- );
474
- });
475
-
476
- test('does not resolve storage.provider="auto" when neither auth nor database is supabase', () => {
477
- const manifest: InfraManifestInput = {
478
- deployment: {
479
- target: 'minikube',
480
- monitoring: false,
481
- },
482
- storage: {
483
- provider: 'auto',
484
- buckets: ['assets'],
485
- },
486
- plugins: [],
487
- };
488
-
489
- const result = generateInfrastructure(manifest, {
490
- namespaceHint: 'shop',
491
- appManifest: createAppManifest('shop'),
492
- });
493
- expect(result.warnings).toContain(
494
- 'Storage provider "auto" could not be resolved for minikube; no storage artifacts generated.',
495
- );
496
- expect(result.files.map((f) => f.path)).not.toContain(
497
- 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
136
+ const storageConfig = getFile(
137
+ result.files,
138
+ 'infra/minikube/k8s/storage/supabase/supabase-storage.configmap.yaml',
498
139
  );
140
+ expect(storageConfig).toContain('namespace: app');
141
+ expect(storageConfig).toContain('STORAGE_BUCKETS: "assets,uploads"');
142
+ expect(storageConfig).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
499
143
  });
500
144
 
501
145
  test('generates route and screen cerbos policies when app manifest context is provided', () => {
@@ -572,28 +216,20 @@ describe('generateInfrastructure', () => {
572
216
  },
573
217
  };
574
218
 
575
- const result = generateInfrastructure(manifest, { namespaceHint: 'shop', appManifest });
576
- const cerbosPolicy = result.files.find(
577
- (f) => f.path === 'infra/minikube/k8s/authz/cerbos/cerbos.policy.configmap.yaml',
219
+ const result = generateInfrastructure(manifest, { appManifest });
220
+ const cerbosPolicy = getFile(
221
+ result.files,
222
+ 'infra/minikube/k8s/authz/cerbos/cerbos.policy.configmap.yaml',
578
223
  );
579
224
 
580
- expect(cerbosPolicy?.content).toContain('app.resource_policy.yaml: |');
581
- expect(cerbosPolicy?.content).toContain('route.resource_policy.yaml: |');
582
- expect(cerbosPolicy?.content).toContain('screen.resource_policy.yaml: |');
583
- expect(cerbosPolicy?.content).toContain(
225
+ expect(cerbosPolicy).toContain('route.resource_policy.yaml: |');
226
+ expect(cerbosPolicy).toContain('screen.resource_policy.yaml: |');
227
+ expect(cerbosPolicy).toContain(
584
228
  'expr: "request.resource.attr.route in [\\"sign-in\\",\\"sign-up\\"]"',
585
229
  );
586
- expect(cerbosPolicy?.content).toContain(
230
+ expect(cerbosPolicy).toContain(
587
231
  'expr: "request.resource.attr.route in [\\"cart\\",\\"index\\"]"',
588
232
  );
589
- expect(cerbosPolicy?.content).toContain(
590
- 'expr: "request.resource.attr.screen_id in [\\"screen-sign-in\\",\\"screen-sign-up\\"]"',
591
- );
592
- expect(cerbosPolicy?.content).toContain(
593
- 'expr: "request.resource.attr.screen_id in [\\"screen-cart\\",\\"screen-home\\"]"',
594
- );
595
- expect(cerbosPolicy?.content).toContain('roles: ["anonymous","authenticated"]');
596
- expect(cerbosPolicy?.content).toContain('roles: ["authenticated"]');
597
233
  });
598
234
 
599
235
  test('throws for missing deployment', () => {
@@ -618,55 +254,7 @@ describe('generateInfrastructure', () => {
618
254
  );
619
255
  });
620
256
 
621
- test('throws for cognito provider until adapter is implemented', () => {
622
- const manifest: InfraManifestInput = {
623
- deployment: {
624
- target: 'minikube',
625
- monitoring: false,
626
- },
627
- auth: {
628
- scope: 'global',
629
- provider: 'cognito',
630
- authorization: {
631
- kind: 'ABAC',
632
- engine: 'cerbos',
633
- },
634
- },
635
- plugins: [],
636
- };
637
-
638
- expect(() => generateInfrastructure(manifest)).toThrow(
639
- 'Unsupported auth provider for minikube adapter: "cognito". Only "supabase" is currently supported.',
640
- );
641
- });
642
-
643
- test('throws for unsupported opa authorization engine', () => {
644
- const manifest: InfraManifestInput = {
645
- deployment: {
646
- target: 'minikube',
647
- monitoring: false,
648
- },
649
- auth: {
650
- scope: 'global',
651
- provider: 'supabase',
652
- authorization: {
653
- kind: 'ABAC',
654
- engine: 'opa' as never,
655
- },
656
- },
657
- database: {
658
- provider: 'supabase',
659
- tier: 'dev',
660
- },
661
- plugins: [],
662
- };
663
-
664
- expect(() =>
665
- generateInfrastructure(manifest, { appManifest: createAppManifest('shop') }),
666
- ).toThrow('Unsupported authorization engine for minikube adapter: opa');
667
- });
668
-
669
- test('requires canonical app slug for local Supabase infrastructure', () => {
257
+ test('requires canonical app slug for minikube infrastructure', () => {
670
258
  const manifest: InfraManifestInput = {
671
259
  deployment: {
672
260
  target: 'minikube',
@@ -680,123 +268,22 @@ describe('generateInfrastructure', () => {
680
268
  };
681
269
 
682
270
  expect(() => generateInfrastructure(manifest, { namespaceHint: 'shop' })).toThrow(
683
- 'Cannot generate local Supabase infrastructure: appManifest.metadata.slug is required.',
271
+ 'Cannot generate Minikube infrastructure: appManifest.metadata.slug is required.',
684
272
  );
685
273
  expect(() =>
686
274
  generateInfrastructure(manifest, {
687
- namespaceHint: 'shop',
688
275
  appManifest: createAppManifest('Scanner App'),
689
276
  }),
690
277
  ).toThrow('appManifest.metadata.slug must be a canonical lowercase slug up to 40 characters');
691
-
692
- const fortyCharacterSlug = 'a'.repeat(40);
693
- const fortyCharacterResult = generateInfrastructure(manifest, {
694
- namespaceHint: 'shop',
695
- appManifest: createAppManifest(fortyCharacterSlug),
696
- });
697
- const fortyCharacterScript = fortyCharacterResult.files.find(
698
- (f) => f.path === 'infra/minikube/scripts/supabase-local-env.sh',
699
- );
700
- expect(fortyCharacterScript?.content).toContain(
701
- `EXPECTED_SUPABASE_PROJECT_ID="${fortyCharacterSlug}"`,
702
- );
703
-
704
- expect(() =>
705
- generateInfrastructure(manifest, {
706
- namespaceHint: 'shop',
707
- appManifest: createAppManifest('a'.repeat(41)),
708
- }),
709
- ).toThrow('appManifest.metadata.slug must be a canonical lowercase slug up to 40 characters');
710
-
711
- expect(() =>
712
- generateInfrastructure(manifest, {
713
- namespaceHint: 'shop',
714
- appManifest: createAppManifest('very-long-project-name-with-identical-prefix-alpha'),
715
- }),
716
- ).toThrow('appManifest.metadata.slug must be a canonical lowercase slug up to 40 characters');
717
- expect(() =>
718
- generateInfrastructure(manifest, {
719
- namespaceHint: 'shop',
720
- appManifest: createAppManifest('very-long-project-name-with-identical-prefix-beta'),
721
- }),
722
- ).toThrow('appManifest.metadata.slug must be a canonical lowercase slug up to 40 characters');
723
- });
724
-
725
- test('keeps Supabase project identity independent from domain-derived namespace', () => {
726
- const manifest: InfraManifestInput = {
727
- deployment: {
728
- target: 'minikube',
729
- monitoring: false,
730
- },
731
- database: {
732
- provider: 'supabase',
733
- tier: 'dev',
734
- },
735
- networking: {
736
- domain: 'local.example.test',
737
- cdn: false,
738
- },
739
- plugins: [],
740
- };
741
-
742
- const result = generateInfrastructure(manifest, {
743
- namespaceHint: 'ignored',
744
- appManifest: createAppManifest('scanner'),
745
- });
746
- const namespaceFile = result.files.find((f) => f.path === 'infra/minikube/k8s/namespace.yaml');
747
- const envExample = result.files.find((f) => f.path === 'infra/minikube/.env.example');
748
- const script = result.files.find(
749
- (f) => f.path === 'infra/minikube/scripts/supabase-local-env.sh',
750
- );
751
-
752
- expect(namespaceFile?.content).toContain('name: local-example-test');
753
- expect(envExample?.content).toContain('SUPABASE_LOCAL_PORT_BASE=64020');
754
- expect(envExample?.content).not.toContain('SUPABASE_LOCAL_PORT_BASE=61550');
755
- expect(script?.content).toContain('EXPECTED_SUPABASE_PROJECT_ID="scanner"');
756
- });
757
-
758
- test('uses namespace hint when domain is not provided', () => {
759
- const manifest: InfraManifestInput = {
760
- deployment: {
761
- target: 'minikube',
762
- monitoring: false,
763
- },
764
- auth: {
765
- scope: 'global',
766
- provider: 'supabase',
767
- authorization: {
768
- kind: 'ABAC',
769
- engine: 'cerbos',
770
- },
771
- },
772
- database: {
773
- provider: 'supabase',
774
- tier: 'dev',
775
- },
776
- plugins: [],
777
- };
778
-
779
- const result = generateInfrastructure(manifest, {
780
- namespaceHint: 'shop',
781
- appManifest: createAppManifest('shop'),
782
- });
783
- const namespaceFile = result.files.find((f) => f.path === 'infra/minikube/k8s/namespace.yaml');
784
- const upScript = result.files.find((f) => f.path === 'infra/minikube/scripts/up.sh');
785
- const statusScript = result.files.find((f) => f.path === 'infra/minikube/scripts/status.sh');
786
-
787
- expect(namespaceFile?.content).toContain('name: shop');
788
- expect(upScript?.content).toContain('NAMESPACE="${ANKH_NAMESPACE:-shop}"');
789
- expect(upScript?.content).toContain('APP_IMAGE="${APP_IMAGE:-ankh/shop:dev}"');
790
- expect(statusScript?.content).toContain('NAMESPACE="${ANKH_NAMESPACE:-shop}"');
791
- expect(statusScript?.content).toContain('kubectl config current-context');
792
- expect(statusScript?.content).toContain('kubectl cluster-info');
793
- expect(statusScript?.content).toContain("Namespace '${NAMESPACE}' was not found");
794
- expect(statusScript?.content).toContain('Runtime auth checks:');
795
- expect(statusScript?.content).toContain('- AUTH_RUNTIME_MODE=${AUTH_RUNTIME_MODE}');
796
- expect(statusScript?.content).toContain('- secret/supabase-auth-secrets: present');
797
278
  });
798
279
  });
799
280
 
281
+ function getFile(files: readonly { path: string; content: string }[], path: string): string {
282
+ const file = files.find((candidate) => candidate.path === path);
283
+ if (!file) throw new Error(`Missing generated file: ${path}`);
284
+ return file.content;
285
+ }
286
+
800
287
  function createAppManifest(
801
288
  slug: string,
802
289
  ): Pick<AppManifest, 'metadata' | 'navigator' | 'screens' | 'settings'> {