@ankhorage/infra 0.4.2 → 1.0.0

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 +8 -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 +9 -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 +2728 -1843
  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 +41 -38
  32. package/src/adapters/minikube/auth/supabase/index.ts +9 -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 +380 -675
  36. package/src/adapters/minikube/base/index.ts +2935 -1878
  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 +63 -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,61 @@ 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).not.toContain('SUPABASE_SERVICE_ROLE_KEY');
310
104
  });
311
105
 
312
106
  test('generates minikube storage artifacts for explicit supabase provider with normalized buckets', () => {
@@ -318,10 +112,6 @@ describe('generateInfrastructure', () => {
318
112
  auth: {
319
113
  scope: 'global',
320
114
  provider: 'supabase',
321
- authorization: {
322
- kind: 'ABAC',
323
- engine: 'cerbos',
324
- },
325
115
  },
326
116
  database: {
327
117
  provider: 'supabase',
@@ -335,7 +125,6 @@ describe('generateInfrastructure', () => {
335
125
  };
336
126
 
337
127
  const result = generateInfrastructure(manifest, {
338
- namespaceHint: 'shop',
339
128
  appManifest: createAppManifest('shop'),
340
129
  });
341
130
 
@@ -343,159 +132,13 @@ describe('generateInfrastructure', () => {
343
132
  'Storage buckets are configured but not created automatically yet. Ensure buckets exist in Supabase Storage: assets,uploads.',
344
133
  );
345
134
 
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',
135
+ const storageConfig = getFile(
136
+ result.files,
137
+ 'infra/minikube/k8s/storage/supabase/supabase-storage.configmap.yaml',
498
138
  );
139
+ expect(storageConfig).toContain('namespace: app');
140
+ expect(storageConfig).toContain('STORAGE_BUCKETS: "assets,uploads"');
141
+ expect(storageConfig).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
499
142
  });
500
143
 
501
144
  test('generates route and screen cerbos policies when app manifest context is provided', () => {
@@ -572,28 +215,20 @@ describe('generateInfrastructure', () => {
572
215
  },
573
216
  };
574
217
 
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',
218
+ const result = generateInfrastructure(manifest, { appManifest });
219
+ const cerbosPolicy = getFile(
220
+ result.files,
221
+ 'infra/minikube/k8s/authz/cerbos/cerbos.policy.configmap.yaml',
578
222
  );
579
223
 
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(
224
+ expect(cerbosPolicy).toContain('route.resource_policy.yaml: |');
225
+ expect(cerbosPolicy).toContain('screen.resource_policy.yaml: |');
226
+ expect(cerbosPolicy).toContain(
584
227
  'expr: "request.resource.attr.route in [\\"sign-in\\",\\"sign-up\\"]"',
585
228
  );
586
- expect(cerbosPolicy?.content).toContain(
229
+ expect(cerbosPolicy).toContain(
587
230
  'expr: "request.resource.attr.route in [\\"cart\\",\\"index\\"]"',
588
231
  );
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
232
  });
598
233
 
599
234
  test('throws for missing deployment', () => {
@@ -618,55 +253,7 @@ describe('generateInfrastructure', () => {
618
253
  );
619
254
  });
620
255
 
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', () => {
256
+ test('requires canonical app slug for minikube infrastructure', () => {
670
257
  const manifest: InfraManifestInput = {
671
258
  deployment: {
672
259
  target: 'minikube',
@@ -680,123 +267,22 @@ describe('generateInfrastructure', () => {
680
267
  };
681
268
 
682
269
  expect(() => generateInfrastructure(manifest, { namespaceHint: 'shop' })).toThrow(
683
- 'Cannot generate local Supabase infrastructure: appManifest.metadata.slug is required.',
270
+ 'Cannot generate Minikube infrastructure: appManifest.metadata.slug is required.',
684
271
  );
685
272
  expect(() =>
686
273
  generateInfrastructure(manifest, {
687
- namespaceHint: 'shop',
688
274
  appManifest: createAppManifest('Scanner App'),
689
275
  }),
690
276
  ).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
277
  });
798
278
  });
799
279
 
280
+ function getFile(files: readonly { path: string; content: string }[], path: string): string {
281
+ const file = files.find((candidate) => candidate.path === path);
282
+ if (!file) throw new Error(`Missing generated file: ${path}`);
283
+ return file.content;
284
+ }
285
+
800
286
  function createAppManifest(
801
287
  slug: string,
802
288
  ): Pick<AppManifest, 'metadata' | 'navigator' | 'screens' | 'settings'> {