@ankhorage/infra 0.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/adapters/minikube/auth/index.d.ts +7 -0
  4. package/dist/adapters/minikube/auth/index.d.ts.map +1 -0
  5. package/dist/adapters/minikube/auth/index.js +14 -0
  6. package/dist/adapters/minikube/auth/index.js.map +1 -0
  7. package/dist/adapters/minikube/auth/supabase/index.d.ts +7 -0
  8. package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -0
  9. package/dist/adapters/minikube/auth/supabase/index.js +213 -0
  10. package/dist/adapters/minikube/auth/supabase/index.js.map +1 -0
  11. package/dist/adapters/minikube/authz/cerbos/index.d.ts +9 -0
  12. package/dist/adapters/minikube/authz/cerbos/index.d.ts.map +1 -0
  13. package/dist/adapters/minikube/authz/cerbos/index.js +360 -0
  14. package/dist/adapters/minikube/authz/cerbos/index.js.map +1 -0
  15. package/dist/adapters/minikube/authz/index.d.ts +9 -0
  16. package/dist/adapters/minikube/authz/index.d.ts.map +1 -0
  17. package/dist/adapters/minikube/authz/index.js +23 -0
  18. package/dist/adapters/minikube/authz/index.js.map +1 -0
  19. package/dist/adapters/minikube/base/index.d.ts +9 -0
  20. package/dist/adapters/minikube/base/index.d.ts.map +1 -0
  21. package/dist/adapters/minikube/base/index.js +1371 -0
  22. package/dist/adapters/minikube/base/index.js.map +1 -0
  23. package/dist/adapters/minikube/contracts.d.ts +9 -0
  24. package/dist/adapters/minikube/contracts.d.ts.map +1 -0
  25. package/dist/adapters/minikube/contracts.js +9 -0
  26. package/dist/adapters/minikube/contracts.js.map +1 -0
  27. package/dist/adapters/minikube/index.d.ts +3 -0
  28. package/dist/adapters/minikube/index.d.ts.map +1 -0
  29. package/dist/adapters/minikube/index.js +114 -0
  30. package/dist/adapters/minikube/index.js.map +1 -0
  31. package/dist/adapters/minikube/storage/index.d.ts +7 -0
  32. package/dist/adapters/minikube/storage/index.d.ts.map +1 -0
  33. package/dist/adapters/minikube/storage/index.js +77 -0
  34. package/dist/adapters/minikube/storage/index.js.map +1 -0
  35. package/dist/adapters/minikube/storage/supabase/index.d.ts +7 -0
  36. package/dist/adapters/minikube/storage/supabase/index.d.ts.map +1 -0
  37. package/dist/adapters/minikube/storage/supabase/index.js +81 -0
  38. package/dist/adapters/minikube/storage/supabase/index.js.map +1 -0
  39. package/dist/datasets.d.ts +11 -0
  40. package/dist/datasets.d.ts.map +1 -0
  41. package/dist/datasets.js +397 -0
  42. package/dist/datasets.js.map +1 -0
  43. package/dist/index.d.ts +7 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +15 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/types.d.ts +70 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +86 -0
  52. package/src/adapters/minikube/README.md +73 -0
  53. package/src/adapters/minikube/auth/index.ts +23 -0
  54. package/src/adapters/minikube/auth/supabase/index.ts +269 -0
  55. package/src/adapters/minikube/authz/cerbos/index.ts +458 -0
  56. package/src/adapters/minikube/authz/index.ts +32 -0
  57. package/src/adapters/minikube/base/index.test.ts +103 -0
  58. package/src/adapters/minikube/base/index.ts +1478 -0
  59. package/src/adapters/minikube/contracts.ts +17 -0
  60. package/src/adapters/minikube/index.ts +149 -0
  61. package/src/adapters/minikube/storage/index.ts +91 -0
  62. package/src/adapters/minikube/storage/supabase/index.ts +112 -0
  63. package/src/datasets.test.ts +178 -0
  64. package/src/datasets.ts +562 -0
  65. package/src/index.test.ts +679 -0
  66. package/src/index.ts +43 -0
  67. package/src/minikubeImageLifecycle.test.ts +34 -0
  68. package/src/types.ts +82 -0
@@ -0,0 +1,679 @@
1
+ import type { AppManifest } from '@ankhorage/contracts';
2
+ import { describe, expect, test } from 'bun:test';
3
+
4
+ import { generateInfrastructure } from './index';
5
+ import type { InfraManifestInput } from './types';
6
+
7
+ describe('generateInfrastructure', () => {
8
+ test('generates minikube artifacts with supabase auth and cerbos authz resources', () => {
9
+ const manifest: InfraManifestInput = {
10
+ deployment: {
11
+ target: 'minikube',
12
+ monitoring: false,
13
+ },
14
+ auth: {
15
+ scope: 'global',
16
+ provider: 'supabase',
17
+ authorization: {
18
+ kind: 'ABAC',
19
+ engine: 'cerbos',
20
+ },
21
+ signIn: {
22
+ identifiers: ['email', 'username'],
23
+ },
24
+ signUp: {
25
+ requiredFields: ['email', 'password'],
26
+ optionalFields: ['firstName', 'lastName'],
27
+ signUpPolicy: 'requireVerification',
28
+ },
29
+ profile: {
30
+ fields: ['email', 'firstName', 'lastName', 'avatarUrl'],
31
+ },
32
+ },
33
+ database: {
34
+ provider: 'supabase',
35
+ tier: 'dev',
36
+ },
37
+ storage: {
38
+ provider: 'auto',
39
+ buckets: ['assets'],
40
+ },
41
+ networking: {
42
+ domain: 'alpha.local',
43
+ cdn: false,
44
+ },
45
+ plugins: [],
46
+ };
47
+
48
+ const result = generateInfrastructure(manifest);
49
+ const paths = result.files.map((f) => f.path);
50
+
51
+ expect(result.meta.target).toBe('minikube');
52
+ expect(result.meta.providers).toEqual(['auto', 'cerbos', 'supabase']);
53
+ expect(paths).toContain('infra/minikube/k8s/namespace.yaml');
54
+ expect(paths).toContain('infra/minikube/k8s/app/deployment.yaml');
55
+ expect(paths).toContain('infra/minikube/k8s/app/service.yaml');
56
+ expect(paths).toContain('infra/minikube/k8s/auth/supabase/supabase-auth.configmap.yaml');
57
+ expect(paths).toContain('infra/minikube/k8s/auth/supabase/app-runtime-auth.env.configmap.yaml');
58
+ expect(paths).toContain('infra/minikube/k8s/authz/cerbos/cerbos.policy.configmap.yaml');
59
+ expect(paths).toContain('infra/minikube/k8s/authz/cerbos/cerbos.deployment.yaml');
60
+ expect(paths).toContain('infra/minikube/auth/supabase-runtime-wiring.md');
61
+ expect(paths).toContain('infra/minikube/k8s/storage/supabase/supabase-storage.configmap.yaml');
62
+ expect(paths).toContain(
63
+ 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
64
+ );
65
+ expect(paths).toContain('infra/minikube/scripts/up.sh');
66
+ expect(paths).toContain('infra/minikube/scripts/build-app-image.sh');
67
+ 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=59120');
151
+ expect(envExample?.content).toContain('SUPABASE_LOCAL_ANALYTICS_PORT=59125');
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('59125');
172
+ expect(minikubeReadme?.content).toContain('$APP_SOURCE_DIR/.env.local');
173
+ expect(minikubeReadme?.content).toContain('apps/card/.env.local');
174
+
175
+ const runtimeGuide = result.files.find(
176
+ (f) => f.path === 'infra/minikube/auth/supabase-runtime-wiring.md',
177
+ );
178
+ expect(runtimeGuide?.content).toContain('envFrom:');
179
+ expect(runtimeGuide?.content).toContain('supabase-auth-secrets');
180
+ expect(runtimeGuide?.content).toContain('AUTH_SIGN_IN_IDENTIFIERS');
181
+ expect(runtimeGuide?.content).toContain('AUTH_SIGN_UP_REQUIRED_FIELDS');
182
+ expect(runtimeGuide?.content).toContain('AUTH_SIGN_UP_OPTIONAL_FIELDS');
183
+ expect(runtimeGuide?.content).toContain('AUTH_SIGN_UP_POLICY');
184
+ expect(runtimeGuide?.content).toContain('AUTH_PROFILE_FIELDS');
185
+
186
+ const supabaseConfigMap = result.files.find(
187
+ (f) => f.path === 'infra/minikube/k8s/auth/supabase/supabase-auth.configmap.yaml',
188
+ );
189
+ expect(supabaseConfigMap?.content).toContain('AUTH_SIGN_IN_IDENTIFIERS: "email,username"');
190
+ expect(supabaseConfigMap?.content).toContain('AUTH_SIGN_UP_REQUIRED_FIELDS: "email,password"');
191
+ expect(supabaseConfigMap?.content).toContain(
192
+ 'AUTH_SIGN_UP_OPTIONAL_FIELDS: "firstName,lastName"',
193
+ );
194
+ expect(supabaseConfigMap?.content).toContain('AUTH_SIGN_UP_POLICY: "requireVerification"');
195
+ expect(supabaseConfigMap?.content).toContain(
196
+ 'AUTH_PROFILE_FIELDS: "email,firstName,lastName,avatarUrl"',
197
+ );
198
+
199
+ const runtimeAuthConfigMap = result.files.find(
200
+ (f) => f.path === 'infra/minikube/k8s/auth/supabase/app-runtime-auth.env.configmap.yaml',
201
+ );
202
+ expect(runtimeAuthConfigMap?.content).toContain('AUTH_SIGN_IN_IDENTIFIERS: "email,username"');
203
+ expect(runtimeAuthConfigMap?.content).toContain(
204
+ 'AUTH_SIGN_UP_REQUIRED_FIELDS: "email,password"',
205
+ );
206
+ expect(runtimeAuthConfigMap?.content).toContain(
207
+ 'AUTH_SIGN_UP_OPTIONAL_FIELDS: "firstName,lastName"',
208
+ );
209
+ expect(runtimeAuthConfigMap?.content).toContain('AUTH_SIGN_UP_POLICY: "requireVerification"');
210
+ expect(runtimeAuthConfigMap?.content).toContain(
211
+ 'AUTH_PROFILE_FIELDS: "email,firstName,lastName,avatarUrl"',
212
+ );
213
+
214
+ const appDeployment = result.files.find(
215
+ (f) => f.path === 'infra/minikube/k8s/app/deployment.yaml',
216
+ );
217
+ expect(appDeployment?.content).toContain('image: ankh/alpha-local:dev');
218
+ expect(appDeployment?.content).toContain('name: app-infra-config');
219
+ expect(appDeployment?.content).toContain('name: app-runtime-auth-env');
220
+ expect(appDeployment?.content).toContain('name: app-runtime-storage-env');
221
+ expect(appDeployment?.content).toContain('name: supabase-auth-secrets');
222
+
223
+ const appDeploymentContent = appDeployment?.content ?? '';
224
+ const appInfraIndex = appDeploymentContent.indexOf('name: app-infra-config');
225
+ const authIndex = appDeploymentContent.indexOf('name: app-runtime-auth-env');
226
+ const storageIndex = appDeploymentContent.indexOf('name: app-runtime-storage-env');
227
+ const supabaseSecretsIndex = appDeploymentContent.indexOf('name: supabase-auth-secrets');
228
+
229
+ // envFrom precedence: later sources win on duplicate keys; explicit `env` entries win over `envFrom`.
230
+ // Keep storage discovery (`app-runtime-storage-env`) before provider secrets to allow future overlap.
231
+ expect(appInfraIndex).toBeGreaterThanOrEqual(0);
232
+ expect(authIndex).toBeGreaterThan(appInfraIndex);
233
+ expect(storageIndex).toBeGreaterThan(authIndex);
234
+ expect(supabaseSecretsIndex).toBeGreaterThan(storageIndex);
235
+
236
+ const appBuildScript = result.files.find(
237
+ (f) => f.path === 'infra/minikube/scripts/build-app-image.sh',
238
+ );
239
+ expect(appBuildScript?.executable).toBe(true);
240
+ expect(appBuildScript?.content).toContain('bunx expo export --platform web --output-dir');
241
+ expect(appBuildScript?.content).toContain('docker build -t "${APP_IMAGE}"');
242
+ expect(appBuildScript?.content).toContain('APP_SOURCE_DIR=');
243
+
244
+ const portForwardScript = result.files.find(
245
+ (f) => f.path === 'infra/minikube/scripts/port-forward.sh',
246
+ );
247
+ expect(portForwardScript?.executable).toBe(true);
248
+ expect(portForwardScript?.content).toContain(
249
+ 'kubectl -n "${NAMESPACE}" port-forward service/app-runtime "${LOCAL_PORT}:${REMOTE_PORT}"',
250
+ );
251
+ expect(portForwardScript?.content).toContain(
252
+ 'LOCAL_PORT="${APP_PORT_FORWARD_LOCAL_PORT:-18080}"',
253
+ );
254
+
255
+ const supabaseLocalEnvScript = result.files.find(
256
+ (f) => f.path === 'infra/minikube/scripts/supabase-local-env.sh',
257
+ );
258
+ expect(supabaseLocalEnvScript?.executable).toBe(true);
259
+ expect(supabaseLocalEnvScript?.content).toContain('supabase status -o env');
260
+ expect(supabaseLocalEnvScript?.content).toContain('supabase init --yes');
261
+ expect(supabaseLocalEnvScript?.content).toContain('supabase start');
262
+ expect(supabaseLocalEnvScript?.content).toContain('SUPABASE_PROJECT_DIR');
263
+ expect(supabaseLocalEnvScript?.content).toContain('SUPABASE_LOCAL_ANALYTICS_PORT');
264
+ expect(supabaseLocalEnvScript?.content).toContain('export SUPABASE_LOCAL_ANALYTICS_PORT');
265
+ expect(supabaseLocalEnvScript?.content).toContain(
266
+ 'Patch("analytics", "port", _env_required("SUPABASE_LOCAL_ANALYTICS_PORT"))',
267
+ );
268
+ expect(supabaseLocalEnvScript?.content).toContain(
269
+ 'upsert_env "${ENV_FILE}" "SUPABASE_LOCAL_ANALYTICS_PORT" "${SUPABASE_LOCAL_ANALYTICS_PORT}"',
270
+ );
271
+
272
+ const appImageDockerfile = result.files.find(
273
+ (f) => f.path === 'infra/minikube/app-image/Dockerfile',
274
+ );
275
+ expect(appImageDockerfile?.content).toContain('FROM nginx:1.27-alpine');
276
+ expect(appImageDockerfile?.content).toContain('listen 8080;');
277
+ expect(appImageDockerfile?.content).toContain('COPY . /usr/share/nginx/html');
278
+
279
+ const cerbosConfig = result.files.find(
280
+ (f) => f.path === 'infra/minikube/k8s/authz/cerbos/cerbos.configmap.yaml',
281
+ );
282
+ const cerbosPolicy = result.files.find(
283
+ (f) => f.path === 'infra/minikube/k8s/authz/cerbos/cerbos.policy.configmap.yaml',
284
+ );
285
+ const cerbosDeployment = result.files.find(
286
+ (f) => f.path === 'infra/minikube/k8s/authz/cerbos/cerbos.deployment.yaml',
287
+ );
288
+
289
+ expect(cerbosConfig?.content).toContain('name: cerbos-config');
290
+ expect(cerbosConfig?.content).toContain('config.yaml: |');
291
+ expect(cerbosConfig?.content).not.toContain('resource_policy.yaml');
292
+ expect(cerbosPolicy?.content).toContain('name: cerbos-policy');
293
+ expect(cerbosPolicy?.content).toContain('app.resource_policy.yaml: |');
294
+ expect(cerbosPolicy?.content).not.toContain('route.resource_policy.yaml: |');
295
+ expect(cerbosPolicy?.content).not.toContain('screen.resource_policy.yaml: |');
296
+ expect(cerbosPolicy?.content).not.toContain('metadata.yaml');
297
+ expect(cerbosDeployment?.content).toContain('name: cerbos-policy');
298
+ });
299
+
300
+ test('generates minikube storage artifacts for explicit supabase provider with normalized buckets', () => {
301
+ const manifest: InfraManifestInput = {
302
+ deployment: {
303
+ target: 'minikube',
304
+ monitoring: false,
305
+ },
306
+ auth: {
307
+ scope: 'global',
308
+ provider: 'supabase',
309
+ authorization: {
310
+ kind: 'ABAC',
311
+ engine: 'cerbos',
312
+ },
313
+ },
314
+ database: {
315
+ provider: 'supabase',
316
+ tier: 'dev',
317
+ },
318
+ storage: {
319
+ provider: 'supabase',
320
+ buckets: ['assets', ' assets ', '', 'assets', 'uploads'],
321
+ },
322
+ plugins: [],
323
+ };
324
+
325
+ const result = generateInfrastructure(manifest, { namespaceHint: 'shop' });
326
+
327
+ expect(result.warnings).toContain(
328
+ 'Storage buckets are configured but not created automatically yet. Ensure buckets exist in Supabase Storage: assets,uploads.',
329
+ );
330
+
331
+ const storageConfig = result.files.find(
332
+ (f) => f.path === 'infra/minikube/k8s/storage/supabase/supabase-storage.configmap.yaml',
333
+ );
334
+ expect(storageConfig?.content).toContain('STORAGE_PROVIDER: "supabase"');
335
+ expect(storageConfig?.content).toContain('STORAGE_BUCKETS: "assets,uploads"');
336
+ expect(storageConfig?.content).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
337
+
338
+ const runtimeStorageEnv = result.files.find(
339
+ (f) =>
340
+ f.path === 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
341
+ );
342
+ expect(runtimeStorageEnv?.content).toContain('STORAGE_RUNTIME_MODE: "local"');
343
+ expect(runtimeStorageEnv?.content).toContain('STORAGE_BUCKETS: "assets,uploads"');
344
+ expect(runtimeStorageEnv?.content).toContain('EXPO_PUBLIC_STORAGE_BUCKETS: "assets,uploads"');
345
+ expect(runtimeStorageEnv?.content).toContain('EXPO_PUBLIC_STORAGE_DEFAULT_BUCKET: "assets"');
346
+
347
+ const appInfraConfigMap = result.files.find(
348
+ (f) => f.path === 'infra/minikube/k8s/app.configmap.yaml',
349
+ );
350
+ expect(appInfraConfigMap?.content).toContain('STORAGE_PROVIDER: "supabase"');
351
+ expect(appInfraConfigMap?.content).toContain('STORAGE_BUCKETS: "assets,uploads"');
352
+ expect(appInfraConfigMap?.content).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
353
+ });
354
+
355
+ test('resolves storage.provider="auto" to supabase when database provider is supabase', () => {
356
+ const manifest: InfraManifestInput = {
357
+ deployment: {
358
+ target: 'minikube',
359
+ monitoring: false,
360
+ },
361
+ database: {
362
+ provider: 'supabase',
363
+ tier: 'dev',
364
+ },
365
+ storage: {
366
+ provider: 'auto',
367
+ buckets: ['assets'],
368
+ },
369
+ plugins: [],
370
+ };
371
+
372
+ const result = generateInfrastructure(manifest, { namespaceHint: 'shop' });
373
+
374
+ expect(result.files.map((f) => f.path)).toContain(
375
+ 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
376
+ );
377
+
378
+ const envExample = result.files.find((f) => f.path === 'infra/minikube/.env.example');
379
+ expect(envExample?.content).toContain('STORAGE_PROVIDER=supabase');
380
+ expect(envExample?.content).toContain('EXPO_PUBLIC_STORAGE_PROVIDER=supabase');
381
+ expect(envExample?.content).toContain('SUPABASE_URL=');
382
+ expect(envExample?.content).toContain('EXPO_PUBLIC_SUPABASE_URL=');
383
+
384
+ const appInfraConfigMap = result.files.find(
385
+ (f) => f.path === 'infra/minikube/k8s/app.configmap.yaml',
386
+ );
387
+ expect(appInfraConfigMap?.content).toContain('STORAGE_PROVIDER: "supabase"');
388
+ expect(appInfraConfigMap?.content).toContain('STORAGE_BUCKETS: "assets"');
389
+ expect(appInfraConfigMap?.content).toContain('STORAGE_DEFAULT_BUCKET: "assets"');
390
+ });
391
+
392
+ test('warns when explicit supabase provider buckets are empty after normalization', () => {
393
+ const manifest: InfraManifestInput = {
394
+ deployment: {
395
+ target: 'minikube',
396
+ monitoring: false,
397
+ },
398
+ auth: {
399
+ scope: 'global',
400
+ provider: 'supabase',
401
+ authorization: {
402
+ kind: 'ABAC',
403
+ engine: 'cerbos',
404
+ },
405
+ },
406
+ database: {
407
+ provider: 'supabase',
408
+ tier: 'dev',
409
+ },
410
+ storage: {
411
+ provider: 'supabase',
412
+ buckets: ['', ' ', ' '],
413
+ },
414
+ plugins: [],
415
+ };
416
+
417
+ const result = generateInfrastructure(manifest, { namespaceHint: 'shop' });
418
+ expect(result.warnings).toContain(
419
+ 'Storage buckets are empty after normalization; no storage artifacts generated.',
420
+ );
421
+ expect(result.files.map((f) => f.path)).not.toContain(
422
+ 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
423
+ );
424
+ });
425
+
426
+ test('warns when auto provider resolves to supabase but buckets are empty after normalization', () => {
427
+ const manifest: InfraManifestInput = {
428
+ deployment: {
429
+ target: 'minikube',
430
+ monitoring: false,
431
+ },
432
+ database: {
433
+ provider: 'supabase',
434
+ tier: 'dev',
435
+ },
436
+ storage: {
437
+ provider: 'auto',
438
+ buckets: [' ', ''],
439
+ },
440
+ plugins: [],
441
+ };
442
+
443
+ const result = generateInfrastructure(manifest, { namespaceHint: 'shop' });
444
+ expect(result.warnings).toContain(
445
+ 'Storage buckets are empty after normalization; no storage artifacts generated.',
446
+ );
447
+ expect(result.files.map((f) => f.path)).not.toContain(
448
+ 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
449
+ );
450
+ });
451
+
452
+ test('does not resolve storage.provider="auto" when neither auth nor database is supabase', () => {
453
+ const manifest: InfraManifestInput = {
454
+ deployment: {
455
+ target: 'minikube',
456
+ monitoring: false,
457
+ },
458
+ storage: {
459
+ provider: 'auto',
460
+ buckets: ['assets'],
461
+ },
462
+ plugins: [],
463
+ };
464
+
465
+ const result = generateInfrastructure(manifest, { namespaceHint: 'shop' });
466
+ expect(result.warnings).toContain(
467
+ 'Storage provider "auto" could not be resolved for minikube; no storage artifacts generated.',
468
+ );
469
+ expect(result.files.map((f) => f.path)).not.toContain(
470
+ 'infra/minikube/k8s/storage/supabase/app-runtime-storage.env.configmap.yaml',
471
+ );
472
+ });
473
+
474
+ test('generates route and screen cerbos policies when app manifest context is provided', () => {
475
+ const manifest: InfraManifestInput = {
476
+ deployment: {
477
+ target: 'minikube',
478
+ monitoring: false,
479
+ },
480
+ auth: {
481
+ scope: 'global',
482
+ provider: 'supabase',
483
+ authorization: {
484
+ kind: 'ABAC',
485
+ engine: 'cerbos',
486
+ },
487
+ },
488
+ database: {
489
+ provider: 'supabase',
490
+ tier: 'dev',
491
+ },
492
+ plugins: [],
493
+ };
494
+
495
+ const appManifest: Pick<AppManifest, 'metadata' | 'navigator' | 'screens' | 'settings'> = {
496
+ metadata: {
497
+ name: 'Shop',
498
+ slug: 'shop',
499
+ version: '1.0.0',
500
+ themeId: 'default',
501
+ },
502
+ navigator: {
503
+ type: 'stack',
504
+ routes: [
505
+ { name: 'sign-in', screenId: 'screen-sign-in' },
506
+ { name: 'sign-up', screenId: 'screen-sign-up', guards: ['public'] },
507
+ { name: 'index', screenId: 'screen-home' },
508
+ { name: 'cart', screenId: 'screen-cart', guards: ['auth'] },
509
+ ],
510
+ },
511
+ screens: {
512
+ 'screen-sign-in': {
513
+ id: 'screen-sign-in',
514
+ name: 'SignIn',
515
+ root: { id: 'sign-in-root', type: 'View', children: [] },
516
+ },
517
+ 'screen-sign-up': {
518
+ id: 'screen-sign-up',
519
+ name: 'SignUp',
520
+ root: { id: 'sign-up-root', type: 'View', children: [] },
521
+ },
522
+ 'screen-home': {
523
+ id: 'screen-home',
524
+ name: 'Home',
525
+ root: { id: 'home-root', type: 'View', children: [] },
526
+ },
527
+ 'screen-cart': {
528
+ id: 'screen-cart',
529
+ name: 'Cart',
530
+ root: { id: 'cart-root', type: 'View', children: [] },
531
+ },
532
+ },
533
+ settings: {
534
+ localization: {
535
+ defaultLocale: 'en',
536
+ locales: ['en'],
537
+ },
538
+ authFlow: {
539
+ signInRoute: '/sign-in',
540
+ signUpRoute: '/sign-up',
541
+ signOutRoute: '/sign-out',
542
+ unauthorizedRoute: '/sign-in',
543
+ postSignInRoute: '/',
544
+ },
545
+ },
546
+ };
547
+
548
+ const result = generateInfrastructure(manifest, { namespaceHint: 'shop', appManifest });
549
+ const cerbosPolicy = result.files.find(
550
+ (f) => f.path === 'infra/minikube/k8s/authz/cerbos/cerbos.policy.configmap.yaml',
551
+ );
552
+
553
+ expect(cerbosPolicy?.content).toContain('app.resource_policy.yaml: |');
554
+ expect(cerbosPolicy?.content).toContain('route.resource_policy.yaml: |');
555
+ expect(cerbosPolicy?.content).toContain('screen.resource_policy.yaml: |');
556
+ expect(cerbosPolicy?.content).toContain(
557
+ 'expr: "request.resource.attr.route in [\\"sign-in\\",\\"sign-up\\"]"',
558
+ );
559
+ expect(cerbosPolicy?.content).toContain(
560
+ 'expr: "request.resource.attr.route in [\\"cart\\",\\"index\\"]"',
561
+ );
562
+ expect(cerbosPolicy?.content).toContain(
563
+ 'expr: "request.resource.attr.screen_id in [\\"screen-sign-in\\",\\"screen-sign-up\\"]"',
564
+ );
565
+ expect(cerbosPolicy?.content).toContain(
566
+ 'expr: "request.resource.attr.screen_id in [\\"screen-cart\\",\\"screen-home\\"]"',
567
+ );
568
+ expect(cerbosPolicy?.content).toContain('roles: ["anonymous","authenticated"]');
569
+ expect(cerbosPolicy?.content).toContain('roles: ["authenticated"]');
570
+ });
571
+
572
+ test('throws for missing deployment', () => {
573
+ const manifest: InfraManifestInput = {
574
+ plugins: [],
575
+ };
576
+
577
+ expect(() => generateInfrastructure(manifest)).toThrow('Deployment configuration is missing.');
578
+ });
579
+
580
+ test('throws for unsupported deployment target', () => {
581
+ const manifest: InfraManifestInput = {
582
+ deployment: {
583
+ target: 'kubernetes',
584
+ monitoring: false,
585
+ },
586
+ plugins: [],
587
+ };
588
+
589
+ expect(() => generateInfrastructure(manifest)).toThrow(
590
+ 'Unsupported deployment target: kubernetes',
591
+ );
592
+ });
593
+
594
+ test('throws for cognito provider until adapter is implemented', () => {
595
+ const manifest: InfraManifestInput = {
596
+ deployment: {
597
+ target: 'minikube',
598
+ monitoring: false,
599
+ },
600
+ auth: {
601
+ scope: 'global',
602
+ provider: 'cognito',
603
+ authorization: {
604
+ kind: 'ABAC',
605
+ engine: 'cerbos',
606
+ },
607
+ },
608
+ plugins: [],
609
+ };
610
+
611
+ expect(() => generateInfrastructure(manifest)).toThrow(
612
+ 'Unsupported auth provider for minikube adapter: "cognito". Only "supabase" is currently supported.',
613
+ );
614
+ });
615
+
616
+ test('throws for unsupported opa authorization engine', () => {
617
+ const manifest: InfraManifestInput = {
618
+ deployment: {
619
+ target: 'minikube',
620
+ monitoring: false,
621
+ },
622
+ auth: {
623
+ scope: 'global',
624
+ provider: 'supabase',
625
+ authorization: {
626
+ kind: 'ABAC',
627
+ engine: 'opa' as never,
628
+ },
629
+ },
630
+ database: {
631
+ provider: 'supabase',
632
+ tier: 'dev',
633
+ },
634
+ plugins: [],
635
+ };
636
+
637
+ expect(() => generateInfrastructure(manifest)).toThrow(
638
+ 'Unsupported authorization engine for minikube adapter: opa',
639
+ );
640
+ });
641
+
642
+ test('uses namespace hint when domain is not provided', () => {
643
+ const manifest: InfraManifestInput = {
644
+ deployment: {
645
+ target: 'minikube',
646
+ monitoring: false,
647
+ },
648
+ auth: {
649
+ scope: 'global',
650
+ provider: 'supabase',
651
+ authorization: {
652
+ kind: 'ABAC',
653
+ engine: 'cerbos',
654
+ },
655
+ },
656
+ database: {
657
+ provider: 'supabase',
658
+ tier: 'dev',
659
+ },
660
+ plugins: [],
661
+ };
662
+
663
+ const result = generateInfrastructure(manifest, { namespaceHint: 'shop' });
664
+ const namespaceFile = result.files.find((f) => f.path === 'infra/minikube/k8s/namespace.yaml');
665
+ const upScript = result.files.find((f) => f.path === 'infra/minikube/scripts/up.sh');
666
+ const statusScript = result.files.find((f) => f.path === 'infra/minikube/scripts/status.sh');
667
+
668
+ expect(namespaceFile?.content).toContain('name: shop');
669
+ expect(upScript?.content).toContain('NAMESPACE="${ANKH_NAMESPACE:-shop}"');
670
+ expect(upScript?.content).toContain('APP_IMAGE="${APP_IMAGE:-ankh/shop:dev}"');
671
+ expect(statusScript?.content).toContain('NAMESPACE="${ANKH_NAMESPACE:-shop}"');
672
+ expect(statusScript?.content).toContain('kubectl config current-context');
673
+ expect(statusScript?.content).toContain('kubectl cluster-info');
674
+ expect(statusScript?.content).toContain("Namespace '${NAMESPACE}' was not found");
675
+ expect(statusScript?.content).toContain('Runtime auth checks:');
676
+ expect(statusScript?.content).toContain('- AUTH_RUNTIME_MODE=${AUTH_RUNTIME_MODE}');
677
+ expect(statusScript?.content).toContain('- secret/supabase-auth-secrets: present');
678
+ });
679
+ });