@ankhorage/infra 0.4.1 → 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 (46) 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 +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 -1835
  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.integration.test.ts +170 -2
  34. package/src/adapters/minikube/auth/supabase/profile.ts +1 -1
  35. package/src/adapters/minikube/authz/cerbos/index.ts +25 -3
  36. package/src/adapters/minikube/base/index.test.ts +380 -579
  37. package/src/adapters/minikube/base/index.ts +2935 -1870
  38. package/src/adapters/minikube/base/twoAppIsolation.integration.test.ts +636 -0
  39. package/src/adapters/minikube/contracts.ts +29 -0
  40. package/src/adapters/minikube/index.ts +18 -44
  41. package/src/adapters/minikube/secrets/index.test.ts +15 -7
  42. package/src/adapters/minikube/secrets/supabase-vault/index.ts +53 -6
  43. package/src/adapters/minikube/storage/supabase/index.ts +1 -5
  44. package/src/apis.test.ts +2 -1
  45. package/src/index.test.ts +63 -577
  46. package/src/minikubeImageLifecycle.test.ts +4 -3
@@ -1,378 +1,431 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
-
6
1
  import { describe, expect, test } from 'bun:test';
7
2
 
3
+ import { generateInfrastructure } from '../../../index';
4
+ import { createAppManifest } from '../../../testSupport';
8
5
  import type { InfraManifestInput } from '../../../types';
9
- import { generateMinikubeBaseArtifacts } from './index';
10
-
11
- describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
12
- test('keeps legacy my-app defaults while allocating a different range for another app', () => {
13
- const myAppEnv = getGeneratedFileContent('my-app', 'infra/minikube/.env.example');
14
- const chessEnv = getGeneratedFileContent('chess', 'infra/minikube/.env.example');
15
-
16
- expect(myAppEnv).toContain('SUPABASE_LOCAL_PORT_BASE=55320');
17
- expect(myAppEnv).toContain('SUPABASE_LOCAL_SHADOW_PORT=55320');
18
- expect(myAppEnv).toContain('SUPABASE_LOCAL_API_PORT=55321');
19
- expect(myAppEnv).toContain('SUPABASE_LOCAL_DB_PORT=55322');
20
- expect(myAppEnv).toContain('SUPABASE_LOCAL_STUDIO_PORT=55323');
21
- expect(myAppEnv).toContain('SUPABASE_LOCAL_INBUCKET_PORT=55324');
22
- expect(myAppEnv).toContain('SUPABASE_LOCAL_ANALYTICS_PORT=55325');
23
6
 
24
- expect(chessEnv).toContain('SUPABASE_LOCAL_PORT_BASE=62200');
25
- expect(chessEnv).toContain('SUPABASE_LOCAL_SHADOW_PORT=62200');
26
- expect(chessEnv).toContain('SUPABASE_LOCAL_API_PORT=62201');
27
- expect(chessEnv).toContain('SUPABASE_LOCAL_DB_PORT=62202');
28
- expect(chessEnv).toContain('SUPABASE_LOCAL_STUDIO_PORT=62203');
29
- expect(chessEnv).toContain('SUPABASE_LOCAL_INBUCKET_PORT=62204');
30
- expect(chessEnv).toContain('SUPABASE_LOCAL_ANALYTICS_PORT=62205');
31
- expect(chessEnv).not.toContain('SUPABASE_LOCAL_DB_PORT=55322');
7
+ describe('generateMinikubeBaseArtifacts app-owned cluster model', () => {
8
+ test('uses the canonical app slug as the only Minikube profile identity', () => {
9
+ const result = generateInfrastructure(createSupabaseManifest(), {
10
+ namespaceHint: 'ignored',
11
+ appManifest: createAppManifest('chess'),
12
+ });
13
+ const envExample = getFile(result.files, 'infra/minikube/.env.example');
14
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
15
+ const downScript = getFile(result.files, 'infra/minikube/scripts/down.sh');
16
+ const destroyScript = getFile(result.files, 'infra/minikube/scripts/destroy.sh');
17
+
18
+ expect(envExample).toContain('ANKH_APP_SLUG=chess');
19
+ expect(envExample).not.toContain('MINIKUBE_PROFILE=');
20
+ expect(upScript).toContain('PROFILE="${ANKH_APP_SLUG:-chess}"');
21
+ expect(upScript).toContain('minikube start -p "${PROFILE}"');
22
+ expect(upScript).not.toContain('PROFILE="${MINIKUBE_PROFILE:-minikube}"');
23
+ expect(downScript).toContain('minikube stop -p "${PROFILE}"');
24
+ expect(destroyScript).toContain('minikube delete -p "${PROFILE}"');
32
25
  });
33
26
 
34
- test('generates bootstrap logic that derives ports from the app port base and preflights collisions', () => {
35
- const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
36
-
37
- expect(script).toContain('SUPABASE_LOCAL_PORT_BASE="${SUPABASE_LOCAL_PORT_BASE:-62200}"');
38
- expect(script).toContain(
39
- 'SUPABASE_LOCAL_DB_PORT="${SUPABASE_LOCAL_DB_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 2))}"',
40
- );
41
- expect(script).toContain('assert_supabase_local_ports_available');
42
- expect(script).toContain('Supabase local port preflight failed for project');
43
- expect(script).toContain(
44
- 'Override SUPABASE_LOCAL_PORT_BASE or the specific SUPABASE_LOCAL_*_PORT values',
45
- );
46
- expect(script).toContain(
47
- 'upsert_env "${ENV_FILE}" "SUPABASE_LOCAL_PORT_BASE" "${SUPABASE_LOCAL_PORT_BASE}"',
48
- );
27
+ test('generates fixed app and supabase namespaces instead of slug namespaces', () => {
28
+ const result = generateInfrastructure(createSupabaseManifest(), {
29
+ appManifest: createAppManifest('uwm'),
30
+ });
31
+ const paths = result.files.map((file) => file.path);
32
+ const appNamespace = getFile(result.files, 'infra/minikube/k8s/namespaces/app.yaml');
33
+ const supabaseNamespace = getFile(result.files, 'infra/minikube/k8s/namespaces/supabase.yaml');
34
+ const appDeployment = getFile(result.files, 'infra/minikube/k8s/app/deployment.yaml');
35
+ const postgres = getFile(result.files, 'infra/minikube/k8s/supabase/postgres.yaml');
36
+
37
+ expect(paths).toContain('infra/minikube/k8s/namespaces/app.yaml');
38
+ expect(paths).toContain('infra/minikube/k8s/namespaces/supabase.yaml');
39
+ expect(paths).not.toContain('infra/minikube/k8s/namespace.yaml');
40
+ expect(appNamespace).toContain('name: app');
41
+ expect(supabaseNamespace).toContain('name: supabase');
42
+ expect(appDeployment).toContain('namespace: app');
43
+ expect(appDeployment).toContain('name: supabase-public-runtime');
44
+ expect(postgres).toContain('namespace: supabase');
49
45
  });
50
46
 
51
- test('uses app project identity for Supabase ports independently from namespace', () => {
52
- const scannerEnv = getGeneratedFileContent(
53
- 'local-example-test',
54
- 'infra/minikube/.env.example',
55
- {},
56
- 'scanner',
57
- );
58
- const scannerNamespace = getGeneratedFileContent(
59
- 'local-example-test',
60
- 'infra/minikube/k8s/namespace.yaml',
61
- {},
62
- 'scanner',
63
- );
64
- const chessEnv = getGeneratedFileContent(
65
- 'local-example-test',
66
- 'infra/minikube/.env.example',
67
- {},
68
- 'chess',
69
- );
70
-
71
- expect(scannerNamespace).toContain('name: local-example-test');
72
- expect(scannerEnv).toContain('SUPABASE_LOCAL_PORT_BASE=64020');
73
- expect(chessEnv).toContain('SUPABASE_LOCAL_PORT_BASE=62200');
74
- expect(scannerEnv).not.toContain('SUPABASE_LOCAL_PORT_BASE=62200');
75
- });
47
+ test('removes host-level Supabase runtime ownership from generated files', () => {
48
+ const result = generateInfrastructure(createSupabaseManifest(), {
49
+ appManifest: createAppManifest('scanner'),
50
+ });
51
+ const serialized = JSON.stringify(result.files);
76
52
 
77
- test('generates canonical Supabase project identity checks', () => {
78
- const script = getGeneratedFileContent(
79
- 'scanner',
53
+ expect(result.files.map((file) => file.path)).not.toContain(
80
54
  'infra/minikube/scripts/supabase-local-env.sh',
81
55
  );
82
- const status = getGeneratedFileContent('scanner', 'infra/minikube/scripts/status.sh');
83
-
84
- expect(script).toContain('EXPECTED_SUPABASE_PROJECT_ID="scanner"');
85
- expect(script).toContain('write_supabase_project_identity_for_new_config');
86
- expect(script).toContain('validate_supabase_project_identity');
87
- expect(script).toContain('project_id = "{expected}"');
88
- expect(script).toContain(
89
- 'The existing local Supabase project belongs to a different identity.',
90
- );
91
- expect(script).toContain(
92
- 'Destroy the invalid local stack and its project-owned resources, then run Infra Up again.',
93
- );
94
- expect(script).toContain(
95
- 'supabase --workdir "${SUPABASE_PROJECT_DIR}" init --yes >/dev/null\n write_supabase_project_identity_for_new_config',
96
- );
97
- expect(script).toContain('validate_supabase_project_identity\nconfigure_supabase_local_ports');
98
- expect(script).toContain(
99
- 'validate_supabase_project_identity\n assert_supabase_local_ports_available',
100
- );
101
- expect(script).toContain('validate_supabase_project_identity\nrun_checked_command');
102
- expect(script).not.toContain('basename "${APP_SOURCE_DIR}"');
103
- expect(script).not.toContain('_minikube');
104
-
105
- expect(status).toContain('EXPECTED_SUPABASE_PROJECT_ID="scanner"');
106
- expect(status).toContain('reject_supabase_project_id_override');
107
- expect(status).toContain('validate_supabase_project_identity');
108
- expect(status).toContain('Supabase project identity mismatch.');
109
- expect(status).toContain('Destroy the invalid local stack and run Infra Up again.');
110
- expect(status).not.toContain('write_supabase_project_identity_for_new_config');
111
- expect(status).not.toContain('configure_supabase_local_ports');
112
- expect(status).not.toContain('_minikube');
56
+ expect(serialized).not.toContain('supabase start');
57
+ expect(serialized).not.toContain('supabase stop');
58
+ expect(serialized).not.toContain('supabase status');
59
+ expect(serialized).not.toContain('SUPABASE_PROJECT_DIR');
60
+ expect(serialized).not.toContain('SUPABASE_LOCAL_PORT');
61
+ expect(serialized).not.toContain('supabase_*_');
62
+ expect(serialized).not.toContain('supabase-auth-secrets');
113
63
  });
114
64
 
115
- test('patches only a newly initialized Supabase config to the canonical project identity', () => {
116
- const run = runGeneratedSupabaseScript({
117
- projectId: 'scanner',
118
- existingConfig: null,
65
+ test('keeps Supabase migration history but executes against the Kubernetes DB URL', () => {
66
+ const result = generateInfrastructure(createSupabaseManifest(), {
67
+ appManifest: createAppManifest('scanner'),
119
68
  });
120
-
121
- expect(run.status).toBe(0);
122
- expect(readFileSync(run.configPath, 'utf-8')).toContain('project_id = "scanner"');
123
- expect(readFileSync(run.supabaseLogPath, 'utf-8')).toContain(
124
- '--workdir ' + run.root + ' init --yes',
125
- );
69
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
70
+ const readme = getFile(result.files, 'infra/minikube/README.md');
71
+
72
+ expect(upScript).toContain('supabase migration up --db-url "${SUPABASE_DB_URL}"');
73
+ expect(upScript).toContain('export SUPABASE_DB_URL PGSSLMODE=disable');
74
+ expect(upScript).not.toContain('migration up --local');
75
+ expect(upScript).toContain('cd "${ROOT_DIR}"');
76
+ expect(upScript).toContain('set_required_env_default POSTGRES_PASSWORD "$(random_hex 32)"');
77
+ expect(upScript).not.toContain('set_env_default POSTGRES_PASSWORD "$(random_base64');
78
+ expect(upScript).toContain(
79
+ 'set_required_env_default SUPABASE_DB_URL "postgres://postgres:${POSTGRES_PASSWORD}@127.0.0.1:${SUPABASE_DB_FORWARD_LOCAL_PORT}/postgres?sslmode=disable"',
80
+ );
81
+ expect(upScript).toContain('bootstrap_supabase_database');
82
+ expect(readme).toContain('Migration authoring/history remains Supabase');
126
83
  });
127
84
 
128
- test('rejects an existing Supabase config with a different project identity before lifecycle work', () => {
129
- const run = runGeneratedSupabaseScript({
130
- projectId: 'scanner',
131
- existingConfig: 'project_id = "minikube"\n[api]\nport = 54321\n',
85
+ test('lists and generates Supabase Kubernetes bootstrap and gateway contract files', () => {
86
+ const result = generateInfrastructure(createSupabaseManifest(), {
87
+ appManifest: createAppManifest('scanner'),
132
88
  });
133
-
134
- expect(run.status).not.toBe(0);
135
- expect(run.combinedOutput).toContain('Supabase project identity mismatch.');
136
- expect(run.combinedOutput).toContain('Expected "scanner", found "minikube".');
137
- expect(run.combinedOutput).toContain(
138
- 'Destroy the invalid local stack and its project-owned resources, then run Infra Up again.',
139
- );
140
- expect(readFileSync(run.configPath, 'utf-8')).toContain('project_id = "minikube"');
141
- expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
89
+ const paths = result.files.map((file) => file.path);
90
+ const readme = getFile(result.files, 'infra/minikube/README.md');
91
+ const kustomization = getFile(result.files, 'infra/minikube/k8s/kustomization.yaml');
92
+ const bootstrap = getFile(result.files, 'infra/minikube/k8s/supabase/bootstrap.sql');
93
+ const postgresInit = getFile(
94
+ result.files,
95
+ 'infra/minikube/k8s/supabase/postgres.init.configmap.yaml',
96
+ );
97
+ const postgres = getFile(result.files, 'infra/minikube/k8s/supabase/postgres.yaml');
98
+ const auth = getFile(result.files, 'infra/minikube/k8s/supabase/auth.yaml');
99
+ const envExample = getFile(result.files, 'infra/minikube/.env.example');
100
+ const gatewayTemplate = getFile(
101
+ result.files,
102
+ 'infra/minikube/k8s/supabase/gateway.template.yml',
103
+ );
104
+ const gateway = getFile(result.files, 'infra/minikube/k8s/supabase/gateway.yaml');
105
+ const storage = getFile(result.files, 'infra/minikube/k8s/supabase/storage.yaml');
106
+ const kongEntrypoint = getFile(result.files, 'infra/minikube/k8s/supabase/kong-entrypoint.sh');
107
+
108
+ expect(paths).toContain('infra/minikube/k8s/supabase/bootstrap.sql');
109
+ expect(paths).toContain('infra/minikube/k8s/supabase/postgres.init.configmap.yaml');
110
+ expect(paths).toContain('infra/minikube/k8s/supabase/gateway.template.yml');
111
+ expect(paths).toContain('infra/minikube/k8s/supabase/kong-entrypoint.sh');
112
+ expect(paths).not.toContain('infra/minikube/k8s/supabase/secrets.yaml');
113
+ expect(readme).toContain('supabase/bootstrap.sql');
114
+ expect(readme).toContain('supabase/postgres.init.configmap.yaml');
115
+ expect(readme).toContain('supabase/gateway.template.yml');
116
+ expect(readme).toContain('supabase/kong-entrypoint.sh');
117
+ expect(kustomization).not.toContain('supabase/secrets.yaml');
118
+ expect(kustomization).not.toContain('gateway.configmap.yaml');
119
+ expect(kustomization).toContain('supabase/postgres.init.configmap.yaml');
120
+ expect(bootstrap).toContain('CREATE SCHEMA IF NOT EXISTS auth');
121
+ expect(bootstrap).toContain(
122
+ 'GRANT anon, authenticated, service_role TO supabase_storage_admin',
123
+ );
124
+ expect(postgresInit).toContain('alter role postgres with superuser');
125
+ expect(postgresInit).toContain('grant pg_read_server_files to supabase_admin');
126
+ expect(postgresInit).toContain('grant execute on function pg_read_file(text) to public');
127
+ expect(postgres).toContain('name: POSTGRES_USER');
128
+ expect(postgres).toContain('value: postgres');
129
+ expect(postgres).toContain('name: PGPORT');
130
+ expect(postgres).toContain(
131
+ 'mountPath: /docker-entrypoint-initdb.d/99-ankhorage-local-dev-extensions.sql',
132
+ );
133
+ expect(postgres).toContain('config_file=/etc/postgresql/postgresql.conf');
134
+ expect(gatewayTemplate).toContain('keyauth_credentials');
135
+ expect(gatewayTemplate).toContain('request-transformer');
136
+ expect(kongEntrypoint).toContain('LUA_AUTH_EXPR');
137
+ expect(auth).toContain('name: GOTRUE_JWT_EXP');
138
+ expect(auth).toContain('key: JWT_EXPIRY');
139
+ expect(auth).toContain(
140
+ 'postgres://supabase_auth_admin:$(POSTGRES_PASSWORD)@postgres.supabase.svc.cluster.local:5432/postgres?search_path=auth&sslmode=disable',
141
+ );
142
+ expect(storage).toContain(
143
+ 'postgres://supabase_storage_admin:$(POSTGRES_PASSWORD)@postgres.supabase.svc.cluster.local:5432/postgres?search_path=storage&sslmode=disable',
144
+ );
145
+ expect(envExample).toContain('PGRST_DB_SCHEMAS=public,storage,graphql_public');
146
+ expect(auth).toContain('name: GOTRUE_EXTERNAL_EMAIL_ENABLED');
147
+ expect(auth).toContain('key: ENABLE_EMAIL_SIGNUP');
148
+ expect(auth).toContain('name: GOTRUE_SMTP_HOST');
149
+ expect(auth).toContain('name: GOTRUE_SMTP_PORT');
150
+ expect(auth).toContain('name: GOTRUE_SMTP_USER');
151
+ expect(auth).toContain('name: GOTRUE_SMTP_PASS');
152
+ expect(gatewayTemplate).toContain('name: realtime-v1-rest-openapi');
153
+ expect(gatewayTemplate).toContain('paths: [/realtime/v1/api/openapi]');
154
+ expect(gatewayTemplate).toContain('name: realtime-v1-rest-tenants');
155
+ expect(gatewayTemplate).toContain('paths: [/realtime/v1/api/tenants]');
156
+ expect(gatewayTemplate).toContain('name: request-termination');
157
+ expect(gatewayTemplate).toContain('status_code: 403');
158
+ expect(gateway).toContain('mountPath: /home/kong/temp.yml');
159
+ expect(gateway).toContain('subPath: temp.yml');
160
+ expect(gateway).toContain('mountPath: /home/kong/kong-entrypoint.sh');
161
+ expect(gateway).toContain('command: ["kong", "health"]');
142
162
  });
143
163
 
144
- test('rejects an existing Supabase config with missing top-level project identity', () => {
145
- const run = runGeneratedSupabaseScript({
146
- projectId: 'scanner',
147
- existingConfig: '[api]\nport = 54321\n',
164
+ test('defines reset separately from down and destroy', () => {
165
+ const result = generateInfrastructure(createSupabaseManifest(), {
166
+ appManifest: createAppManifest('uwm'),
148
167
  });
149
-
150
- expect(run.status).not.toBe(0);
151
- expect(run.combinedOutput).toContain('Expected "scanner", found missing.');
152
- expect(readFileSync(run.configPath, 'utf-8')).not.toContain('project_id = "scanner"');
153
- expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
168
+ const resetScript = getFile(result.files, 'infra/minikube/scripts/reset.sh');
169
+ const downScript = getFile(result.files, 'infra/minikube/scripts/down.sh');
170
+
171
+ expect(resetScript).toContain('ANKH_RESET_CONFIRM');
172
+ expect(resetScript).toContain('delete namespace app');
173
+ expect(resetScript).toContain('delete namespace supabase');
174
+ expect(resetScript).toContain('does not delete the Minikube profile');
175
+ expect(downScript).toContain('minikube stop -p "${PROFILE}"');
176
+ expect(downScript).not.toContain('delete namespace');
154
177
  });
155
178
 
156
- test('rejects ambient SUPABASE_PROJECT_ID before Supabase CLI operations', () => {
157
- const run = runGeneratedSupabaseScript({
158
- projectId: 'scanner',
159
- existingConfig: 'project_id = "scanner"\n[api]\nport = 54321\n',
160
- env: {
161
- SUPABASE_PROJECT_ID: 'minikube',
179
+ test('keeps reset scoped to app namespace when Supabase is not generated', () => {
180
+ const result = generateInfrastructure(
181
+ {
182
+ deployment: {
183
+ target: 'minikube',
184
+ monitoring: false,
185
+ },
186
+ },
187
+ {
188
+ appManifest: createAppManifest('plain'),
162
189
  },
163
- });
164
-
165
- expect(run.status).not.toBe(0);
166
- expect(run.combinedOutput).toContain(
167
- 'SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.',
168
190
  );
169
- expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
170
- });
191
+ const paths = result.files.map((file) => file.path);
192
+ const resetScript = getFile(result.files, 'infra/minikube/scripts/reset.sh');
193
+ const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh');
171
194
 
172
- test('rejects empty Supabase project identity before Supabase CLI operations', () => {
173
- const run = runGeneratedSupabaseScript({
174
- projectId: 'plain',
175
- supabaseProjectId: null,
176
- existingConfig: null,
177
- });
195
+ expect(paths).not.toContain('infra/minikube/k8s/namespaces/supabase.yaml');
196
+ expect(resetScript).toContain('delete namespace app');
197
+ expect(resetScript).not.toContain('delete namespace supabase');
198
+ expect(resetScript).not.toContain('namespaces/supabase.yaml');
199
+ expect(statusScript).toContain('for namespace in app; do');
200
+ });
178
201
 
179
- expect(run.status).not.toBe(0);
180
- expect(run.combinedOutput).toContain(
181
- 'Cannot run local Supabase infrastructure: expected Supabase project identity is empty.',
202
+ test('places standalone provider workloads in provider-owned namespaces', () => {
203
+ const result = generateInfrastructure(
204
+ {
205
+ ...createSupabaseManifest(),
206
+ auth: {
207
+ scope: 'global',
208
+ provider: 'supabase',
209
+ authorization: {
210
+ engine: 'cerbos',
211
+ kind: 'RBAC',
212
+ },
213
+ },
214
+ },
215
+ {
216
+ appManifest: createAppManifest('authz-app'),
217
+ },
182
218
  );
183
- expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
184
- expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('init --yes');
185
- expect(() => readFileSync(run.configPath, 'utf-8')).toThrow();
219
+ const paths = result.files.map((file) => file.path);
220
+ const kustomization = getFile(result.files, 'infra/minikube/k8s/kustomization.yaml');
221
+ const readme = getFile(result.files, 'infra/minikube/README.md');
222
+ const cerbosDeployment = getFile(
223
+ result.files,
224
+ 'infra/minikube/k8s/authz/cerbos/cerbos.deployment.yaml',
225
+ );
226
+ const envExample = getFile(result.files, 'infra/minikube/.env.example');
227
+ const resetScript = getFile(result.files, 'infra/minikube/scripts/reset.sh');
228
+ const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh');
229
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
230
+
231
+ expect(paths).toContain('infra/minikube/k8s/namespaces/cerbos.yaml');
232
+ expect(kustomization).toContain('namespaces/cerbos.yaml');
233
+ expect(readme).toContain('- Provider namespaces: `cerbos`');
234
+ expect(readme).toContain('## Provider Lifecycle Contributions');
235
+ expect(readme).toContain('`cerbos`: namespace `cerbos`');
236
+ expect(readme).toContain('`deployment/cerbos` in namespace `cerbos`');
237
+ expect(readme).toContain('`http` = `http://cerbos.cerbos.svc.cluster.local:3592`');
238
+ expect(cerbosDeployment).toContain('namespace: cerbos');
239
+ expect(cerbosDeployment).not.toContain('namespace: app');
240
+ expect(envExample).toContain('CERBOS_URL=http://cerbos.cerbos.svc.cluster.local:3592');
241
+ expect(resetScript).toContain('delete namespace cerbos');
242
+ expect(upScript).toContain('wait_for_provider_readiness');
243
+ expect(upScript).toContain('run_provider_migrations');
244
+ expect(upScript).toContain('run_provider_reconciliation');
245
+ expect(upScript).toContain('rollout status "deployment/cerbos" --timeout=180s');
246
+ expect(upScript.indexOf('kubectl --context "${PROFILE}" apply -k "${K8S_DIR}"')).toBeLessThan(
247
+ upScript.lastIndexOf('wait_for_provider_readiness'),
248
+ );
249
+ expect(statusScript).toContain('for namespace in app supabase cerbos; do');
250
+ expect(statusScript).toContain(
251
+ '- provider cerbos endpoint/http: http://cerbos.cerbos.svc.cluster.local:3592',
252
+ );
253
+ expect(statusScript).toContain('- provider cerbos/cerbos: ready');
186
254
  });
187
255
 
188
- test('requires python before Supabase init mutates the workdir', () => {
189
- const run = runGeneratedSupabaseScript({
190
- projectId: 'scanner',
191
- existingConfig: null,
192
- withoutPython: true,
256
+ test('uses generated Supabase runtime ownership as an immutable script decision', () => {
257
+ const result = generateInfrastructure(createSupabaseManifest(), {
258
+ appManifest: createAppManifest('scanner'),
193
259
  });
260
+ const envExample = getFile(result.files, 'infra/minikube/.env.example');
261
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
194
262
 
195
- expect(run.status).not.toBe(0);
196
- expect(run.combinedOutput).toContain(
197
- 'python3 is required to reconcile supabase/config.toml before local Supabase startup.',
198
- );
199
- expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
200
- expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('init --yes');
201
- expect(() => readFileSync(run.configPath, 'utf-8')).toThrow();
263
+ expect(envExample).not.toContain('SUPABASE_RUNTIME_ENABLED=');
264
+ expect(upScript).toContain('SUPABASE_RUNTIME_ENABLED="true"');
265
+ expect(upScript).not.toContain('SUPABASE_RUNTIME_ENABLED="${SUPABASE_RUNTIME_ENABLED');
202
266
  });
203
267
 
204
- test('generates bootstrap logic that retries unhealthy local Supabase startup once', () => {
205
- const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
206
-
207
- expect(script).toContain('start_supabase_local_stack() {');
208
- expect(script).toContain(
209
- 'Supabase local start failed. Stopping stale local stack and retrying once...',
210
- );
211
- expect(script).toContain(
212
- 'supabase --workdir "${SUPABASE_PROJECT_DIR}" stop --no-backup >/dev/null 2>&1 || true',
268
+ test('wires configured OAuth providers into GoTrue runtime env without serializing secrets', () => {
269
+ const result = generateInfrastructure(
270
+ {
271
+ ...createSupabaseManifest(),
272
+ secretStore: {
273
+ provider: 'supabase-vault',
274
+ },
275
+ auth: {
276
+ scope: 'global',
277
+ provider: 'supabase',
278
+ oauth: {
279
+ enabled: true,
280
+ callbackRoute: '/auth/callback',
281
+ providers: [
282
+ {
283
+ id: 'google',
284
+ enabled: true,
285
+ credentialsRef: 'auth/oauth/google',
286
+ },
287
+ ],
288
+ },
289
+ },
290
+ },
291
+ {
292
+ appManifest: createAppManifest('oauth-app'),
293
+ },
213
294
  );
214
- expect(script).toContain('Supabase local start failed after retry.');
215
- expect(script).toContain('start_supabase_local_stack');
216
- expect(
217
- countOccurrences(script, 'supabase --workdir "${SUPABASE_PROJECT_DIR}" start >/dev/null'),
218
- ).toBe(2);
219
- expect(
220
- countOccurrences(script, 'supabase --workdir "${SUPABASE_PROJECT_DIR}" stop --no-backup'),
221
- ).toBe(1);
295
+ const envExample = getFile(result.files, 'infra/minikube/.env.example');
296
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
297
+
298
+ expect(envExample).toContain('# google: auth/oauth/google');
299
+ expect(envExample).toContain('GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=');
300
+ expect(envExample).toContain('GOTRUE_EXTERNAL_GOOGLE_SECRET=');
301
+ expect(upScript).toContain('GOTRUE_EXTERNAL_GOOGLE_ENABLED=true');
302
+ expect(upScript).toContain(
303
+ 'GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=${GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID}',
304
+ );
305
+ expect(upScript).toContain('credentialsRef auth/oauth/google');
306
+ expect(upScript).toContain("resolve_vault_secret_field 'auth/oauth/google' 'clientSecret'");
307
+ expect(upScript).toContain(
308
+ 'set_required_env_default ADDITIONAL_REDIRECT_URLS "$(derive_additional_redirect_urls)"',
309
+ );
310
+ expect(upScript).toContain('site_url_without_slash}/${callback_path}');
311
+ expect(upScript).toContain('OAUTH_NATIVE_REDIRECT_URLS');
312
+ expect(upScript).not.toContain('write_env_value GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID');
313
+ expect(upScript).not.toContain('write_env_value GOTRUE_EXTERNAL_GOOGLE_SECRET');
314
+ expect(upScript).not.toContain('sentinel-client-secret-value');
222
315
  });
223
316
 
224
- test('generates canonical Supabase workdir and required CLI capability checks', () => {
225
- const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
317
+ test('generates required secrets even when copied env example contains empty keys', () => {
318
+ const result = generateInfrastructure(createSupabaseManifest(), {
319
+ appManifest: createAppManifest('scanner'),
320
+ });
321
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
226
322
 
227
- expect(script).toContain('SUPABASE_PROJECT_DIR="${SUPABASE_PROJECT_DIR:-${ROOT_DIR}}"');
228
- expect(script).toContain('require_expected_supabase_project_identity');
229
- expect(script).toContain(
230
- 'require_expected_supabase_project_identity\nreject_supabase_project_id_override\nrequire_supabase_cli_capabilities',
323
+ expect(upScript).toContain('load_env_file_preserving_process_env');
324
+ expect(upScript).toContain('APP_IMAGE="${APP_IMAGE:-}"');
325
+ expect(upScript).toContain(
326
+ 'load_env_file_preserving_process_env\nSUPABASE_RUNTIME_ENABLED="true"',
231
327
  );
232
- expect(script).toContain('supabase CLI >= 2.106.0 is required');
233
- expect(script).toContain('supabase CLI does not support required global --workdir flag');
234
- expect(script).toContain('supabase CLI does not support required migration up --local command');
235
- expect(script).toContain(
236
- 'python3 is required to reconcile supabase/config.toml before local Supabase startup.',
328
+ expect(upScript.indexOf('load_env_file_preserving_process_env')).toBeLessThan(
329
+ upScript.lastIndexOf('APP_IMAGE="${APP_IMAGE:-ankh/scanner:dev}"'),
237
330
  );
238
- expect(script).toContain('psql is required but not installed');
239
- expect(script).toContain('supabase --workdir "${SUPABASE_PROJECT_DIR}" init --yes');
240
- expect(script).toContain('supabase --workdir "${SUPABASE_PROJECT_DIR}" migration up --local');
241
- expect(script).toContain('psql "${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "${sql_file}"');
331
+ expect(upScript).toContain('set_required_env_default POSTGRES_PASSWORD "$(random_hex 32)"');
332
+ expect(upScript).toContain('write_env_value "${key}" "${value}"');
333
+ expect(upScript).toContain('set_optional_env_default SMTP_USER ""');
242
334
  });
243
335
 
244
- test('generates profile reconciliation and live verification flow when profile table is configured', () => {
245
- const overrides = {
246
- auth: {
247
- profile: {
248
- table: 'profiles',
249
- fields: ['email', 'displayName'],
250
- },
251
- },
252
- } satisfies Partial<InfraManifestInput>;
253
- const script = getGeneratedFileContent(
254
- 'chess',
255
- 'infra/minikube/scripts/supabase-local-env.sh',
256
- overrides,
257
- );
258
- const status = getGeneratedFileContent('chess', 'infra/minikube/scripts/status.sh', overrides);
336
+ test('starts Supabase schema owners before app migrations and profile reconciliation', () => {
337
+ const result = generateInfrastructure(createSupabaseManifest(), {
338
+ appManifest: createAppManifest('scanner'),
339
+ });
340
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
259
341
 
260
- expect(script).toContain('SUPABASE_PROFILE_ENABLED="true"');
261
- expect(script).toContain(
262
- 'SUPABASE_PROFILE_RECONCILE_FILE="${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"',
263
- );
264
- expect(script).toContain('Generated profile reconciliation');
265
- expect(script).toContain('SQL file: ${sql_file}');
266
- expect(script).toContain('Supabase project workdir: ${SUPABASE_PROJECT_DIR}');
267
- expect(script).toContain('Exit status: ${status}');
268
- expect(script).toContain('ankhorage_internal.generated_schema_state');
269
- expect(script).toContain('check_immutable_migrations_applied');
270
- expect(status).toContain('- immutable migrations: applied');
271
- expect(status).toContain('- immutable migrations: pending');
272
- expect(status).toContain('- profile reconciliation: applied, checksum matches');
273
- expect(status).toContain('- profile schema: verified');
274
- expect(status).toContain('reserved conflicting identity table public.users exists');
275
- expect(status).toContain('stale managed profile column');
276
- expect(status).toContain('own-profile SELECT policy is missing or has unsafe definition');
277
- expect(status).toContain('unexpected profile table RLS policy exists');
278
- expect(status).toContain(
279
- "has_any_column_privilege('anon', format('public.%I', profile_table), 'SELECT')",
342
+ expect(upScript).toContain('apply_supabase_runtime_workloads');
343
+ expect(upScript).toContain('exec deployment/postgres -- psql -U postgres');
344
+ expect(upScript).toContain('create schema if not exists vault');
345
+ expect(upScript).toContain('grant execute on function pg_read_file(text) to public');
346
+ expect(upScript.indexOf('postgres.init.configmap.yaml')).toBeLessThan(
347
+ upScript.indexOf('postgres.pvc.yaml'),
280
348
  );
281
- expect(status).toContain(
282
- "has_any_column_privilege('anon', format('public.%I', profile_table), 'INSERT')",
349
+ expect(upScript.lastIndexOf(' wait_for_supabase_database')).toBeLessThan(
350
+ upScript.lastIndexOf(' bootstrap_supabase_database'),
283
351
  );
284
- expect(status).toContain(
285
- "has_any_column_privilege('anon', format('public.%I', profile_table), 'UPDATE')",
352
+ expect(upScript).toContain('rollout status deployment/postgres --timeout=900s');
353
+ expect(upScript).toContain('rollout status deployment/gateway --timeout=600s');
354
+ const runMigrationsCallIndex = upScript.lastIndexOf(' run_supabase_migrations');
355
+ const reconcileProfileCallIndex = upScript.lastIndexOf(' reconcile_supabase_profile');
356
+ const appRuntimeApplyIndex = upScript.lastIndexOf(
357
+ 'kubectl --context "${PROFILE}" apply -k "${K8S_DIR}"',
286
358
  );
287
- expect(status).toContain(
288
- "has_any_column_privilege('anon', format('public.%I', profile_table), 'REFERENCES')",
289
- );
290
- expect(status).toContain(
291
- "has_any_column_privilege('authenticated', format('public.%I', profile_table), 'INSERT')",
359
+
360
+ expect(upScript.lastIndexOf(' apply_supabase_runtime_workloads')).toBeLessThan(
361
+ upScript.indexOf('rollout status deployment/auth'),
292
362
  );
293
- expect(status).toContain(
294
- "has_any_column_privilege('authenticated', format('public.%I', profile_table), 'REFERENCES')",
363
+ expect(upScript.indexOf('rollout status deployment/auth')).toBeLessThan(runMigrationsCallIndex);
364
+ expect(upScript.indexOf('rollout status deployment/storage')).toBeLessThan(
365
+ runMigrationsCallIndex,
295
366
  );
296
- expect(status).toContain(
297
- 'authenticated role must not have profile table INSERT, DELETE, or REFERENCES privilege',
367
+ expect(upScript.indexOf(' reconcile_supabase_profile')).toBeLessThan(
368
+ upScript.indexOf(' reload_postgrest_schema'),
298
369
  );
299
- expect(status).toContain(
300
- 'authenticated role has unexpected UPDATE privilege on profile column',
370
+ expect(upScript.indexOf('rollout status deployment/studio')).toBeLessThan(
371
+ upScript.indexOf('"${PORT_FORWARD_SCRIPT}" start studio'),
301
372
  );
302
- expect(status).toContain('generated trigger function execute privilege must be revoked');
373
+ expect(runMigrationsCallIndex).toBeLessThan(reconcileProfileCallIndex);
374
+ expect(reconcileProfileCallIndex).toBeLessThan(appRuntimeApplyIndex);
303
375
  });
304
376
 
305
- test('does not reject SUPABASE_PROJECT_ID for non-Supabase status checks', () => {
306
- const run = runGeneratedStatusScript({
307
- manifest: {
308
- deployment: {
309
- target: 'minikube',
310
- monitoring: false,
311
- },
312
- plugins: [],
313
- },
314
- supabaseProjectId: null,
315
- env: {
316
- SUPABASE_PROJECT_ID: 'ambient-project',
317
- },
377
+ test('treats port-forwards as slug-owned lifecycle resources', () => {
378
+ const result = generateInfrastructure(createSupabaseManifest(), {
379
+ appManifest: createAppManifest('uwm'),
318
380
  });
319
-
320
- expect(run.status).toBe(0);
321
- expect(run.combinedOutput).not.toContain(
322
- 'SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.',
323
- );
324
- expect(run.kubectlLog).toContain('get all -n plain');
325
- });
326
-
327
- test('rejects empty Supabase project identity before local Supabase status checks', () => {
328
- const run = runGeneratedStatusScript({
329
- manifest: {
330
- deployment: {
331
- target: 'minikube',
332
- monitoring: false,
333
- },
334
- plugins: [],
335
- },
336
- supabaseProjectId: null,
337
- env: {
338
- SUPABASE_LOCAL_ENABLED: 'true',
339
- },
340
- });
341
-
342
- expect(run.status).not.toBe(0);
343
- expect(run.combinedOutput).toContain(
344
- 'Cannot run local Supabase infrastructure: expected Supabase project identity is empty.',
345
- );
346
- expect(run.supabaseLog).toBe('');
381
+ const portForwardScript = getFile(result.files, 'infra/minikube/scripts/port-forward.sh');
382
+ const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh');
383
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
384
+ const downScript = getFile(result.files, 'infra/minikube/scripts/down.sh');
385
+
386
+ expect(portForwardScript).toContain('service/app-runtime');
387
+ expect(portForwardScript).toContain('service/gateway');
388
+ expect(portForwardScript).toContain('service/studio');
389
+ expect(portForwardScript).toContain('service/postgres');
390
+ expect(portForwardScript).toContain('${PROFILE}-${1}.pid');
391
+ expect(portForwardScript).toContain('crashed stale_pid');
392
+ expect(portForwardScript).toContain('is_port_accepting()');
393
+ expect(portForwardScript).toContain('recent kubectl port-forward log');
394
+ expect(portForwardScript).toContain('did not become ready on local port');
395
+ expect(portForwardScript).toContain('not ready pid=');
396
+ expect(portForwardScript).toContain('terminate_forward_process()');
397
+ expect(portForwardScript).toContain('ps -p "${pid}" -o command=');
398
+ expect(portForwardScript).toContain('ps -p "${pid}" -o comm=');
399
+ expect(portForwardScript).toContain('is_owned_forward_process()');
400
+ expect(portForwardScript).toContain(
401
+ '*"kubectl --context ${PROFILE} -n ${namespace} port-forward ${resource} ${local_port}:${remote_port}"*',
402
+ );
403
+ expect(portForwardScript).toContain('kill -KILL "${pid}"');
404
+ expect(portForwardScript).toContain('failed to stop owned port-forward pid');
405
+ expect(portForwardScript).toContain('local port ${local_port} is still accepting connections');
406
+ expect(portForwardScript).toContain('refusing to terminate non-owned live pid');
407
+ expect(portForwardScript).toContain(
408
+ 'stale pid file pointed at non-owned live pid ${pid}; removing ownership record ${pid_file}',
409
+ );
410
+ expect(portForwardScript).toContain(
411
+ 'port occupied by non-owned process on local port ${local_port}',
412
+ );
413
+ expect(portForwardScript).toContain('stale_pid_non_owned=${pid}');
414
+ expect(statusScript).toContain('port-forward.sh" status all');
415
+ expect(upScript).toContain('set -Eeuo pipefail');
416
+ expect(upScript).toContain('trap cleanup_failed_up ERR');
417
+ expect(upScript).toContain('start supabase-gateway');
418
+ expect(upScript).toContain('start db-migration');
419
+ expect(upScript).toContain('Failed to start db-migration port-forward for Supabase bootstrap.');
420
+ expect(upScript.indexOf('trap cleanup_failed_up ERR')).toBeLessThan(
421
+ upScript.indexOf('require_command minikube'),
422
+ );
423
+ expect(upScript).toContain('trap - ERR');
424
+ expect(downScript).toContain('port-forward.sh" stop all');
347
425
  });
348
426
  });
349
427
 
350
- function getGeneratedFileContent(
351
- namespace: string,
352
- filePath: string,
353
- overrides: Partial<InfraManifestInput> = {},
354
- supabaseProjectId = namespace,
355
- ): string {
356
- const artifact = generateMinikubeBaseArtifacts({
357
- manifest: createInfraManifest(overrides),
358
- namespace,
359
- supabaseProjectId,
360
- extraResources: [],
361
- extraEnvEntries: [],
362
- }).find((file) => file.path === filePath);
363
-
364
- if (!artifact) {
365
- throw new Error(`Expected generated artifact at ${filePath}`);
366
- }
367
-
368
- return artifact.content;
369
- }
370
-
371
- function countOccurrences(source: string, needle: string): number {
372
- return source.split(needle).length - 1;
373
- }
374
-
375
- function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): InfraManifestInput {
428
+ function createSupabaseManifest(): InfraManifestInput {
376
429
  return {
377
430
  deployment: {
378
431
  target: 'minikube',
@@ -381,273 +434,21 @@ function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): Infra
381
434
  auth: {
382
435
  scope: 'global',
383
436
  provider: 'supabase',
384
- authorization: {
385
- kind: 'RBAC',
386
- engine: 'native',
387
- },
388
437
  },
389
438
  database: {
390
439
  provider: 'supabase',
391
440
  tier: 'dev',
392
441
  },
393
- networking: {
394
- cdn: false,
395
- },
396
- plugins: [],
397
- ...overrides,
398
- auth: {
399
- scope: 'global',
442
+ storage: {
400
443
  provider: 'supabase',
401
- authorization: {
402
- kind: 'RBAC',
403
- engine: 'native',
404
- },
405
- ...overrides.auth,
406
- },
407
- };
408
- }
409
-
410
- function runGeneratedSupabaseScript(args: {
411
- projectId: string;
412
- supabaseProjectId?: string | null;
413
- existingConfig: string | null;
414
- env?: Record<string, string>;
415
- withoutPython?: boolean;
416
- }): {
417
- status: number | null;
418
- combinedOutput: string;
419
- root: string;
420
- configPath: string;
421
- supabaseLogPath: string;
422
- } {
423
- const tempRoot = mkdtempSync(join(tmpdir(), 'infra-supabase-identity-'));
424
- const root = join(tempRoot, 'infra', 'minikube');
425
- const scriptsDir = join(root, 'scripts');
426
- const supabaseDir = join(root, 'supabase');
427
- const binDir = join(tempRoot, 'bin');
428
- const appSourceDir = join(tempRoot, 'app');
429
- const scriptPath = join(scriptsDir, 'supabase-local-env.sh');
430
- const configPath = join(supabaseDir, 'config.toml');
431
- const supabaseLogPath = join(tempRoot, 'supabase.log');
432
-
433
- mkdirSync(scriptsDir, { recursive: true });
434
- mkdirSync(binDir, { recursive: true });
435
- mkdirSync(appSourceDir, { recursive: true });
436
- writeFileSync(join(root, '.env.example'), '');
437
- writeFileSync(supabaseLogPath, '');
438
-
439
- if (args.existingConfig !== null) {
440
- mkdirSync(supabaseDir, { recursive: true });
441
- writeFileSync(configPath, args.existingConfig);
442
- }
443
-
444
- const script = getGeneratedFileContent(
445
- args.projectId,
446
- 'infra/minikube/scripts/supabase-local-env.sh',
447
- {},
448
- args.supabaseProjectId === undefined ? args.projectId : args.supabaseProjectId,
449
- );
450
- writeFileSync(scriptPath, script);
451
- chmodSync(scriptPath, 0o755);
452
-
453
- const supabaseStubPath = join(binDir, 'supabase');
454
- writeFileSync(
455
- supabaseStubPath,
456
- `#!/bin/bash
457
- set -euo pipefail
458
- printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
459
-
460
- if [[ "\${1:-}" == "--version" ]]; then
461
- echo "supabase 2.106.0"
462
- exit 0
463
- fi
464
-
465
- if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
466
- echo "--workdir"
467
- exit 0
468
- fi
469
-
470
- if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
471
- echo "--local"
472
- exit 0
473
- fi
474
-
475
- if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "init" && "\${4:-}" == "--yes" ]]; then
476
- mkdir -p "\${2}/supabase"
477
- printf '%s\\n' 'project_id = "minikube"' '[api]' 'port = 54321' > "\${2}/supabase/config.toml"
478
- exit 0
479
- fi
480
-
481
- if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
482
- cat <<'ENV'
483
- DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
484
- API_URL=http://127.0.0.1:54321
485
- ANON_KEY=anon
486
- SERVICE_ROLE_KEY=service
487
- JWT_SECRET=secret
488
- ENV
489
- exit 0
490
- fi
491
-
492
- if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
493
- exit 0
494
- fi
495
-
496
- if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "migration" && "\${4:-}" == "up" && "\${5:-}" == "--local" ]]; then
497
- exit 0
498
- fi
499
-
500
- echo "unexpected supabase call: $*" >&2
501
- exit 1
502
- `,
503
- );
504
- chmodSync(supabaseStubPath, 0o755);
505
-
506
- const psqlStubPath = join(binDir, 'psql');
507
- writeFileSync(
508
- psqlStubPath,
509
- `#!/bin/bash
510
- exit 0
511
- `,
512
- );
513
- chmodSync(psqlStubPath, 0o755);
514
-
515
- const result = spawnSync('/bin/bash', [scriptPath], {
516
- cwd: tempRoot,
517
- env: {
518
- ...(args.withoutPython ? {} : process.env),
519
- PATH: args.withoutPython ? binDir : `${binDir}:${process.env.PATH ?? ''}`,
520
- APP_SOURCE_DIR: appSourceDir,
521
- SUPABASE_STUB_LOG: supabaseLogPath,
522
- ...args.env,
444
+ buckets: ['assets'],
523
445
  },
524
- encoding: 'utf-8',
525
- });
526
-
527
- return {
528
- status: result.status,
529
- combinedOutput: `${result.stdout}${result.stderr}`,
530
- root,
531
- configPath,
532
- supabaseLogPath,
446
+ plugins: [],
533
447
  };
534
448
  }
535
449
 
536
- function runGeneratedStatusScript(args: {
537
- manifest: InfraManifestInput;
538
- supabaseProjectId: string | null;
539
- env?: Record<string, string>;
540
- }): {
541
- status: number | null;
542
- combinedOutput: string;
543
- kubectlLog: string;
544
- supabaseLog: string;
545
- } {
546
- const tempRoot = mkdtempSync(join(tmpdir(), 'infra-status-'));
547
- const root = join(tempRoot, 'infra', 'minikube');
548
- const scriptsDir = join(root, 'scripts');
549
- const binDir = join(tempRoot, 'bin');
550
- const scriptPath = join(scriptsDir, 'status.sh');
551
- const kubectlLogPath = join(tempRoot, 'kubectl.log');
552
- const supabaseLogPath = join(tempRoot, 'supabase.log');
553
-
554
- mkdirSync(scriptsDir, { recursive: true });
555
- mkdirSync(binDir, { recursive: true });
556
- writeFileSync(join(root, '.env.example'), '');
557
- writeFileSync(kubectlLogPath, '');
558
- writeFileSync(supabaseLogPath, '');
559
-
560
- const artifact = generateMinikubeBaseArtifacts({
561
- manifest: args.manifest,
562
- namespace: 'plain',
563
- supabaseProjectId: args.supabaseProjectId,
564
- extraResources: [],
565
- extraEnvEntries: [],
566
- }).find((file) => file.path === 'infra/minikube/scripts/status.sh');
567
-
568
- if (!artifact) {
569
- throw new Error('Expected generated status.sh artifact');
570
- }
571
-
572
- writeFileSync(scriptPath, artifact.content);
573
- chmodSync(scriptPath, 0o755);
574
-
575
- const kubectlStubPath = join(binDir, 'kubectl');
576
- writeFileSync(
577
- kubectlStubPath,
578
- `#!/bin/bash
579
- set -euo pipefail
580
- printf '%s\\n' "$*" >> "\${KUBECTL_STUB_LOG}"
581
-
582
- if [[ "\${1:-}" == "config" && "\${2:-}" == "current-context" ]]; then
583
- echo "minikube"
584
- exit 0
585
- fi
586
-
587
- if [[ "\${1:-}" == "cluster-info" ]]; then
588
- echo "Kubernetes control plane is running"
589
- exit 0
590
- fi
591
-
592
- if [[ "\${1:-}" == "get" && "\${2:-}" == "namespace" ]]; then
593
- exit 0
594
- fi
595
-
596
- if [[ "\${1:-}" == "get" && "\${2:-}" == "all" ]]; then
597
- echo "No resources found"
598
- exit 0
599
- fi
600
-
601
- echo "unexpected kubectl call: $*" >&2
602
- exit 1
603
- `,
604
- );
605
- chmodSync(kubectlStubPath, 0o755);
606
-
607
- const supabaseStubPath = join(binDir, 'supabase');
608
- writeFileSync(
609
- supabaseStubPath,
610
- `#!/bin/bash
611
- set -euo pipefail
612
- printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
613
-
614
- if [[ "\${1:-}" == "--version" ]]; then
615
- echo "supabase 2.106.0"
616
- exit 0
617
- fi
618
-
619
- if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
620
- echo "--workdir"
621
- exit 0
622
- fi
623
-
624
- if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
625
- echo "--local"
626
- exit 0
627
- fi
628
-
629
- echo "unexpected supabase call: $*" >&2
630
- exit 1
631
- `,
632
- );
633
- chmodSync(supabaseStubPath, 0o755);
634
-
635
- const result = spawnSync('/bin/bash', [scriptPath], {
636
- cwd: tempRoot,
637
- env: {
638
- ...process.env,
639
- PATH: `${binDir}:${process.env.PATH ?? ''}`,
640
- KUBECTL_STUB_LOG: kubectlLogPath,
641
- SUPABASE_STUB_LOG: supabaseLogPath,
642
- ...args.env,
643
- },
644
- encoding: 'utf-8',
645
- });
646
-
647
- return {
648
- status: result.status,
649
- combinedOutput: `${result.stdout}${result.stderr}`,
650
- kubectlLog: readFileSync(kubectlLogPath, 'utf-8'),
651
- supabaseLog: readFileSync(supabaseLogPath, 'utf-8'),
652
- };
450
+ function getFile(files: readonly { path: string; content: string }[], path: string): string {
451
+ const file = files.find((candidate) => candidate.path === path);
452
+ if (!file) throw new Error(`Missing generated file: ${path}`);
453
+ return file.content;
653
454
  }