@ankhorage/infra 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/dist/adapters/minikube/base/index.d.ts +1 -0
  4. package/dist/adapters/minikube/base/index.d.ts.map +1 -1
  5. package/dist/adapters/minikube/base/index.js +239 -22
  6. package/dist/adapters/minikube/base/index.js.map +1 -1
  7. package/dist/adapters/minikube/index.d.ts.map +1 -1
  8. package/dist/adapters/minikube/index.js +34 -0
  9. package/dist/adapters/minikube/index.js.map +1 -1
  10. package/dist/adapters/minikube/secrets/index.d.ts +7 -0
  11. package/dist/adapters/minikube/secrets/index.d.ts.map +1 -0
  12. package/dist/adapters/minikube/secrets/index.js +13 -0
  13. package/dist/adapters/minikube/secrets/index.js.map +1 -0
  14. package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts +8 -0
  15. package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts.map +1 -0
  16. package/dist/adapters/minikube/secrets/supabase-vault/index.js +74 -0
  17. package/dist/adapters/minikube/secrets/supabase-vault/index.js.map +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/secretStore.d.ts +19 -0
  23. package/dist/secretStore.d.ts.map +1 -0
  24. package/dist/secretStore.js +22 -0
  25. package/dist/secretStore.js.map +1 -0
  26. package/package.json +7 -3
  27. package/src/adapters/minikube/base/index.test.ts +456 -0
  28. package/src/adapters/minikube/base/index.ts +244 -20
  29. package/src/adapters/minikube/index.ts +44 -0
  30. package/src/adapters/minikube/secrets/index.test.ts +139 -0
  31. package/src/adapters/minikube/secrets/index.ts +19 -0
  32. package/src/adapters/minikube/secrets/supabase-vault/index.ts +91 -0
  33. package/src/index.test.ts +151 -12
  34. package/src/index.ts +5 -0
  35. package/src/package.test.ts +8 -1
  36. package/src/secretStore.test.ts +76 -0
  37. package/src/secretStore.ts +44 -0
@@ -1,3 +1,8 @@
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
+
1
6
  import { describe, expect, test } from 'bun:test';
2
7
 
3
8
  import type { InfraManifestInput } from '../../../types';
@@ -43,6 +48,159 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
43
48
  );
44
49
  });
45
50
 
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
+ });
76
+
77
+ test('generates canonical Supabase project identity checks', () => {
78
+ const script = getGeneratedFileContent(
79
+ 'scanner',
80
+ 'infra/minikube/scripts/supabase-local-env.sh',
81
+ );
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');
113
+ });
114
+
115
+ test('patches only a newly initialized Supabase config to the canonical project identity', () => {
116
+ const run = runGeneratedSupabaseScript({
117
+ projectId: 'scanner',
118
+ existingConfig: null,
119
+ });
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
+ );
126
+ });
127
+
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',
132
+ });
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');
142
+ });
143
+
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',
148
+ });
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');
154
+ });
155
+
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',
162
+ },
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
+ );
169
+ expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
170
+ });
171
+
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
+ });
178
+
179
+ expect(run.status).not.toBe(0);
180
+ expect(run.combinedOutput).toContain(
181
+ 'Cannot run local Supabase infrastructure: expected Supabase project identity is empty.',
182
+ );
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();
186
+ });
187
+
188
+ test('requires python before Supabase init mutates the workdir', () => {
189
+ const run = runGeneratedSupabaseScript({
190
+ projectId: 'scanner',
191
+ existingConfig: null,
192
+ withoutPython: true,
193
+ });
194
+
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();
202
+ });
203
+
46
204
  test('generates bootstrap logic that retries unhealthy local Supabase startup once', () => {
47
205
  const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
48
206
 
@@ -67,9 +225,16 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
67
225
  const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
68
226
 
69
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',
231
+ );
70
232
  expect(script).toContain('supabase CLI >= 2.106.0 is required');
71
233
  expect(script).toContain('supabase CLI does not support required global --workdir flag');
72
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.',
237
+ );
73
238
  expect(script).toContain('psql is required but not installed');
74
239
  expect(script).toContain('supabase --workdir "${SUPABASE_PROJECT_DIR}" init --yes');
75
240
  expect(script).toContain('supabase --workdir "${SUPABASE_PROJECT_DIR}" migration up --local');
@@ -136,16 +301,62 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
136
301
  );
137
302
  expect(status).toContain('generated trigger function execute privilege must be revoked');
138
303
  });
304
+
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
+ },
318
+ });
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('');
347
+ });
139
348
  });
140
349
 
141
350
  function getGeneratedFileContent(
142
351
  namespace: string,
143
352
  filePath: string,
144
353
  overrides: Partial<InfraManifestInput> = {},
354
+ supabaseProjectId = namespace,
145
355
  ): string {
146
356
  const artifact = generateMinikubeBaseArtifacts({
147
357
  manifest: createInfraManifest(overrides),
148
358
  namespace,
359
+ supabaseProjectId,
149
360
  extraResources: [],
150
361
  extraEnvEntries: [],
151
362
  }).find((file) => file.path === filePath);
@@ -195,3 +406,248 @@ function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): Infra
195
406
  },
196
407
  };
197
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,
523
+ },
524
+ encoding: 'utf-8',
525
+ });
526
+
527
+ return {
528
+ status: result.status,
529
+ combinedOutput: `${result.stdout}${result.stderr}`,
530
+ root,
531
+ configPath,
532
+ supabaseLogPath,
533
+ };
534
+ }
535
+
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
+ };
653
+ }