@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,18 +1,30 @@
1
1
  import { execFile as execFileCallback } from 'node:child_process';
2
- import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
 
7
7
  import { describe, expect, test } from 'bun:test';
8
8
 
9
- import { generateInfrastructure } from '../../../../index';
9
+ import { generateInfrastructure as generateRawInfrastructure } from '../../../../index';
10
+ import { createAppManifest } from '../../../../testSupport';
10
11
  import type { GeneratedInfrastructureFile, InfraManifestInput } from '../../../../types';
11
12
 
12
13
  const execFile = promisify(execFileCallback);
13
14
  const INTEGRATION_TIMEOUT_MS = 300_000;
14
15
  const integrationTest = process.env.ANKH_SUPABASE_INTEGRATION === '1' ? test : test.skip;
15
16
 
17
+ function generateInfrastructure(
18
+ manifest: InfraManifestInput,
19
+ options: { namespaceHint?: string } = {},
20
+ ): ReturnType<typeof generateRawInfrastructure> {
21
+ const slug = options.namespaceHint ?? 'profile-integration';
22
+ return generateRawInfrastructure(manifest, {
23
+ ...options,
24
+ appManifest: createAppManifest(slug),
25
+ });
26
+ }
27
+
16
28
  describe('generated Supabase profile reconciliation integration', () => {
17
29
  integrationTest(
18
30
  'drops stale managed columns while preserving custom columns',
@@ -132,6 +144,109 @@ $$;`,
132
144
  INTEGRATION_TIMEOUT_MS,
133
145
  );
134
146
 
147
+ integrationTest(
148
+ 'boots clean disabled profile and writes client env idempotently',
149
+ async () => {
150
+ const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
151
+
152
+ try {
153
+ await writeGeneratedFiles(
154
+ appRoot,
155
+ generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
156
+ namespaceHint,
157
+ }).files,
158
+ );
159
+
160
+ await runLocalSupabaseEnv(minikubeRoot);
161
+ await runLocalSupabaseEnv(minikubeRoot);
162
+
163
+ const { stdout: statusEnv } = await execFile('supabase', [
164
+ '--workdir',
165
+ minikubeRoot,
166
+ 'status',
167
+ '-o',
168
+ 'env',
169
+ ]);
170
+ const expectedUrl = readStatusEnv(statusEnv, 'API_URL');
171
+ const expectedAnonKey = readStatusEnv(statusEnv, 'ANON_KEY');
172
+ const appEnv = await readFile(path.join(appRoot, '.env.local'), 'utf8');
173
+
174
+ expect(readStatusEnv(appEnv, 'EXPO_PUBLIC_SUPABASE_URL')).toBe(expectedUrl);
175
+ expect(readStatusEnv(appEnv, 'EXPO_PUBLIC_SUPABASE_ANON_KEY')).toBe(expectedAnonKey);
176
+ } finally {
177
+ await cleanupIntegrationProject(appRoot, minikubeRoot);
178
+ }
179
+ },
180
+ INTEGRATION_TIMEOUT_MS,
181
+ );
182
+
183
+ integrationTest(
184
+ 'accepts disabled profile when state table exists without auth profile state',
185
+ async () => {
186
+ const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
187
+
188
+ try {
189
+ await writeGeneratedFiles(
190
+ appRoot,
191
+ generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
192
+ namespaceHint,
193
+ }).files,
194
+ );
195
+
196
+ await runLocalSupabaseEnv(minikubeRoot);
197
+ await runSupabaseSql(
198
+ minikubeRoot,
199
+ `create schema if not exists ankhorage_internal;
200
+ create table if not exists ankhorage_internal.generated_schema_state (
201
+ artifact_key text primary key,
202
+ table_name text,
203
+ content_hash text,
204
+ updated_at timestamptz not null default now()
205
+ );
206
+ insert into ankhorage_internal.generated_schema_state (
207
+ artifact_key,
208
+ table_name,
209
+ content_hash
210
+ )
211
+ values ('storage.bucket', 'objects', repeat('1', 64))
212
+ on conflict (artifact_key) do update
213
+ set table_name = excluded.table_name,
214
+ content_hash = excluded.content_hash,
215
+ updated_at = now();`,
216
+ );
217
+
218
+ await runLocalSupabaseEnv(minikubeRoot);
219
+ } finally {
220
+ await cleanupIntegrationProject(appRoot, minikubeRoot);
221
+ }
222
+ },
223
+ INTEGRATION_TIMEOUT_MS,
224
+ );
225
+
226
+ integrationTest(
227
+ 'status reports clean disabled profile as skipped',
228
+ async () => {
229
+ const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
230
+
231
+ try {
232
+ await writeGeneratedFiles(
233
+ appRoot,
234
+ generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
235
+ namespaceHint,
236
+ }).files,
237
+ );
238
+ await runLocalSupabaseEnv(minikubeRoot);
239
+
240
+ const output = await runGeneratedStatus(minikubeRoot);
241
+ expect(output).toContain('- profile reconciliation: skipped (no profile table configured)');
242
+ expect(output).toContain('- profile schema: skipped (no profile table configured)');
243
+ } finally {
244
+ await cleanupIntegrationProject(appRoot, minikubeRoot);
245
+ }
246
+ },
247
+ INTEGRATION_TIMEOUT_MS,
248
+ );
249
+
135
250
  integrationTest(
136
251
  'rejects profile table rename once generated state exists',
137
252
  async () => {
@@ -789,6 +904,59 @@ exit 0
789
904
  throw new Error('Expected generated status script to fail');
790
905
  }
791
906
 
907
+ async function runGeneratedStatus(minikubeRoot: string): Promise<string> {
908
+ const fakeBin = path.join(minikubeRoot, 'fake-status-bin');
909
+ const fakeKubectl = path.join(fakeBin, 'kubectl');
910
+ await mkdir(fakeBin, { recursive: true });
911
+ await writeFile(
912
+ fakeKubectl,
913
+ `#!/usr/bin/env bash
914
+ set -euo pipefail
915
+
916
+ if [[ "\${1:-}" == "config" && "\${2:-}" == "current-context" ]]; then
917
+ echo fake-context
918
+ exit 0
919
+ fi
920
+
921
+ if [[ "\${1:-}" == "cluster-info" ]]; then
922
+ echo "Kubernetes control plane is running"
923
+ exit 0
924
+ fi
925
+
926
+ if [[ "\${1:-}" == "get" && "\${2:-}" == "namespace" ]]; then
927
+ exit 0
928
+ fi
929
+
930
+ if [[ "\${1:-}" == "get" && "\${2:-}" == "all" ]]; then
931
+ exit 0
932
+ fi
933
+
934
+ if [[ "\${1:-}" == "-n" && "\${3:-}" == "get" && "\${4:-}" == "configmap" ]]; then
935
+ exit 0
936
+ fi
937
+
938
+ if [[ "\${1:-}" == "-n" && "\${3:-}" == "get" && "\${4:-}" == "secret" ]]; then
939
+ exit 1
940
+ fi
941
+
942
+ exit 0
943
+ `,
944
+ 'utf8',
945
+ );
946
+ await chmod(fakeKubectl, 0o755);
947
+
948
+ const result = await execFile(path.join(minikubeRoot, 'scripts/status.sh'), {
949
+ cwd: minikubeRoot,
950
+ env: {
951
+ ...process.env,
952
+ PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
953
+ SUPABASE_PROJECT_DIR: minikubeRoot,
954
+ },
955
+ });
956
+
957
+ return `${result.stdout}\n${result.stderr}`;
958
+ }
959
+
792
960
  async function runSupabaseSql(minikubeRoot: string, sql: string): Promise<void> {
793
961
  const { stdout } = await execFile('supabase', ['--workdir', minikubeRoot, 'status', '-o', 'env']);
794
962
  const dbUrl = readStatusEnv(stdout, 'DB_URL');
@@ -10,7 +10,7 @@ const PROFILE_POLICY_VERSION = 1;
10
10
  const PROFILE_RECONCILER_VERSION = 1;
11
11
  const PROFILE_ARTIFACT_KEY = 'auth.profile';
12
12
 
13
- export const MANAGED_PROFILE_COLUMNS = [
13
+ const MANAGED_PROFILE_COLUMNS = [
14
14
  'email',
15
15
  'display_name',
16
16
  'first_name',
@@ -2,19 +2,40 @@ import type { AppManifest, NavigatorSpec, RouteDefinition } from '@ankhorage/con
2
2
  import { resolveAuthFlow } from '@ankhorage/contracts/auth';
3
3
 
4
4
  import type { InfraManifestInput } from '../../../../types';
5
- import type { MinikubeAdapterArtifacts } from '../../contracts';
5
+ import type { MinikubeAdapterArtifacts, MinikubeProviderLifecycle } from '../../contracts';
6
+
7
+ const CERBOS_NAMESPACE = 'cerbos';
6
8
 
7
9
  export function generateCerbosAuthzArtifacts(args: {
8
10
  manifest: InfraManifestInput;
9
11
  namespace: string;
10
12
  appManifest?: CerbosAppManifest;
11
13
  }): MinikubeAdapterArtifacts {
12
- const { manifest, namespace, appManifest } = args;
14
+ const { manifest, appManifest } = args;
15
+ const namespace = CERBOS_NAMESPACE;
13
16
 
14
17
  const root = 'infra/minikube/k8s/authz/cerbos';
15
18
  const resourceRoot = 'authz/cerbos';
16
19
  const intent = buildCerbosPolicyIntent({ manifest, appManifest });
17
20
 
21
+ const endpointUrl = `http://cerbos.${namespace}.svc.cluster.local:3592`;
22
+ const lifecycle: MinikubeProviderLifecycle = {
23
+ id: 'cerbos',
24
+ namespace,
25
+ endpoints: [{ name: 'http', url: endpointUrl }],
26
+ readinessChecks: [
27
+ {
28
+ label: 'cerbos',
29
+ namespace,
30
+ resource: 'deployment/cerbos',
31
+ timeoutSeconds: 180,
32
+ },
33
+ ],
34
+ migrationCommands: [],
35
+ reconciliationCommands: [],
36
+ statusChecks: [],
37
+ };
38
+
18
39
  return {
19
40
  files: [
20
41
  {
@@ -40,7 +61,8 @@ export function generateCerbosAuthzArtifacts(args: {
40
61
  `${resourceRoot}/cerbos.deployment.yaml`,
41
62
  `${resourceRoot}/cerbos.service.yaml`,
42
63
  ],
43
- envEntries: ['CERBOS_URL=http://cerbos:3592'],
64
+ providerLifecycle: [lifecycle],
65
+ envEntries: [`CERBOS_URL=${endpointUrl}`],
44
66
  warnings: [],
45
67
  };
46
68
  }