@ankhorage/infra 0.4.2 → 1.0.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 (45) 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 +14 -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 +2764 -1841
  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 +46 -38
  32. package/src/adapters/minikube/auth/supabase/index.ts +14 -40
  33. package/src/adapters/minikube/auth/supabase/profile.ts +1 -1
  34. package/src/adapters/minikube/authz/cerbos/index.ts +25 -3
  35. package/src/adapters/minikube/base/index.test.ts +399 -673
  36. package/src/adapters/minikube/base/index.ts +2969 -1874
  37. package/src/adapters/minikube/base/twoAppIsolation.integration.test.ts +636 -0
  38. package/src/adapters/minikube/contracts.ts +29 -0
  39. package/src/adapters/minikube/index.ts +18 -44
  40. package/src/adapters/minikube/secrets/index.test.ts +15 -7
  41. package/src/adapters/minikube/secrets/supabase-vault/index.ts +53 -6
  42. package/src/adapters/minikube/storage/supabase/index.ts +1 -5
  43. package/src/apis.test.ts +2 -1
  44. package/src/index.test.ts +64 -577
  45. package/src/minikubeImageLifecycle.test.ts +4 -3
@@ -7,11 +7,10 @@ import type {
7
7
  } from '../../types';
8
8
  import { generateAuthProviderArtifacts } from './auth';
9
9
  import { generateAuthorizationArtifacts } from './authz';
10
- import { generateMinikubeBaseArtifacts } from './base';
10
+ import { APP_NAMESPACE, generateMinikubeBaseArtifacts } from './base';
11
11
  import { generateSecretStoreArtifacts } from './secrets';
12
12
  import { generateStorageArtifacts } from './storage';
13
13
 
14
- const DEFAULT_NAMESPACE = 'ankh-app';
15
14
  const CANONICAL_PROJECT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
16
15
 
17
16
  export function generateMinikubeInfra(
@@ -30,10 +29,8 @@ export function generateMinikubeInfra(
30
29
  );
31
30
  }
32
31
 
33
- const namespace = resolveNamespace(manifest, options);
34
- const supabaseProjectId = isSupabaseLocalEnabled(manifest)
35
- ? validateCanonicalProjectSlug(options.appManifest?.metadata.slug)
36
- : null;
32
+ const appSlug = validateCanonicalAppSlug(options.appManifest?.metadata.slug);
33
+ const namespace = APP_NAMESPACE;
37
34
 
38
35
  const authArtifacts = generateAuthProviderArtifacts({ manifest, namespace });
39
36
  const authzArtifacts = generateAuthorizationArtifacts({
@@ -54,6 +51,15 @@ export function generateMinikubeInfra(
54
51
  ...storageArtifacts.resources,
55
52
  ...secretStoreArtifacts.resources,
56
53
  ]);
54
+ const providerLifecycle = [
55
+ ...authArtifacts.providerLifecycle,
56
+ ...authzArtifacts.providerLifecycle,
57
+ ...storageArtifacts.providerLifecycle,
58
+ ...secretStoreArtifacts.providerLifecycle,
59
+ ];
60
+ const providerNamespaces = unique([
61
+ ...providerLifecycle.map((contribution) => contribution.namespace),
62
+ ]);
57
63
  const extraEnvEntries = unique([
58
64
  ...authArtifacts.envEntries,
59
65
  ...authzArtifacts.envEntries,
@@ -63,9 +69,10 @@ export function generateMinikubeInfra(
63
69
 
64
70
  const baseFiles = generateMinikubeBaseArtifacts({
65
71
  manifest,
66
- namespace,
67
- supabaseProjectId,
72
+ appSlug,
68
73
  extraResources,
74
+ providerNamespaces,
75
+ providerLifecycle,
69
76
  extraEnvEntries,
70
77
  });
71
78
 
@@ -141,55 +148,22 @@ function collectWarnings(manifest: InfraManifestInput): string[] {
141
148
  return warnings;
142
149
  }
143
150
 
144
- function isSupabaseLocalEnabled(manifest: InfraManifestInput): boolean {
145
- const authProvider = manifest.auth?.provider;
146
- const databaseProvider = manifest.database?.provider;
147
- const storageProvider = manifest.storage?.provider;
148
- const secretStoreProvider = manifest.secretStore?.provider;
149
-
150
- return (
151
- authProvider === 'supabase' ||
152
- databaseProvider === 'supabase' ||
153
- storageProvider === 'supabase' ||
154
- (storageProvider === 'auto' &&
155
- (authProvider === 'supabase' || databaseProvider === 'supabase')) ||
156
- secretStoreProvider === 'supabase-vault'
157
- );
158
- }
159
-
160
- function validateCanonicalProjectSlug(slug: string | undefined): string {
151
+ function validateCanonicalAppSlug(slug: string | undefined): string {
161
152
  if (!slug) {
162
153
  throw new Error(
163
- 'Cannot generate local Supabase infrastructure: appManifest.metadata.slug is required.',
154
+ 'Cannot generate Minikube infrastructure: appManifest.metadata.slug is required.',
164
155
  );
165
156
  }
166
157
 
167
158
  if (slug.trim() !== slug || !CANONICAL_PROJECT_SLUG_RE.test(slug)) {
168
159
  throw new Error(
169
- 'Cannot generate local Supabase infrastructure: appManifest.metadata.slug must be a canonical lowercase slug up to 40 characters using only a-z, 0-9, and hyphens, without leading or trailing hyphens.',
160
+ 'Cannot generate Minikube infrastructure: appManifest.metadata.slug must be a canonical lowercase slug up to 40 characters using only a-z, 0-9, and hyphens, without leading or trailing hyphens.',
170
161
  );
171
162
  }
172
163
 
173
164
  return slug;
174
165
  }
175
166
 
176
- function resolveNamespace(
177
- manifest: InfraManifestInput,
178
- options: InfrastructureGenerationOptions,
179
- ): string {
180
- const source =
181
- manifest.networking?.domain?.trim() ?? options.namespaceHint?.trim() ?? DEFAULT_NAMESPACE;
182
-
183
- const normalized = source
184
- .toLowerCase()
185
- .replace(/[^a-z0-9-]/g, '-')
186
- .replace(/-+/g, '-')
187
- .replace(/^-|-$/g, '');
188
-
189
- const safe = normalized.slice(0, 63).replace(/-+$/g, '');
190
- return safe || DEFAULT_NAMESPACE;
191
- }
192
-
193
167
  function unique(values: string[]): string[] {
194
168
  return [...new Set(values)];
195
169
  }
@@ -1,5 +1,4 @@
1
1
  import type { AppManifest } from '@ankhorage/contracts';
2
- import { SUPABASE_VAULT_MIGRATION_SQL } from '@ankhorage/supabase-vault';
3
2
  import { describe, expect, test } from 'bun:test';
4
3
 
5
4
  import { generateInfrastructure } from '../../../index';
@@ -21,7 +20,7 @@ function createManifest(overrides: Partial<InfraManifestInput> = {}): InfraManif
21
20
  }
22
21
 
23
22
  describe('minikube secret-store generation', () => {
24
- test('generates the released Supabase Vault migration through the existing lifecycle', () => {
23
+ test('generates the released Supabase Vault migration through the Kubernetes lifecycle', () => {
25
24
  const result = generateInfrastructure(createManifest(), {
26
25
  appManifest: createAppManifest('vault-app'),
27
26
  });
@@ -29,10 +28,18 @@ describe('minikube secret-store generation', () => {
29
28
  const envExample = result.files.find((file) => file.path === 'infra/minikube/.env.example');
30
29
  const upScript = result.files.find((file) => file.path === 'infra/minikube/scripts/up.sh');
31
30
 
32
- expect(migration?.content).toBe(`${SUPABASE_VAULT_MIGRATION_SQL.trim()}\n`);
31
+ expect(migration?.content).toContain('create schema if not exists vault;');
32
+ expect(migration?.content).toContain(
33
+ 'create extension if not exists supabase_vault with schema vault;',
34
+ );
35
+ expect(migration?.content).toContain('when insufficient_privilege then');
36
+ expect(migration?.content).toContain('create or replace function vault.create_secret');
37
+ expect(migration?.content).toContain('create or replace view vault.decrypted_secrets');
38
+ expect(migration?.content).not.toContain('supabase_vault with schema extensions');
33
39
  expect(envExample?.content).toContain('SECRET_STORE_PROVIDER=supabase-vault');
34
- expect(envExample?.content).toContain('SUPABASE_LOCAL_ENABLED=true');
35
- expect(upScript?.content).toContain('SUPABASE_LOCAL_ENABLED="${SUPABASE_LOCAL_ENABLED:-true}"');
40
+ expect(envExample?.content).not.toContain('SUPABASE_RUNTIME_ENABLED=');
41
+ expect(upScript?.content).toContain('SUPABASE_RUNTIME_ENABLED="true"');
42
+ expect(upScript?.content).not.toContain('SUPABASE_RUNTIME_ENABLED="${SUPABASE_RUNTIME_ENABLED');
36
43
  expect(result.meta.providers).toContain('supabase-vault');
37
44
  });
38
45
 
@@ -90,8 +97,8 @@ describe('minikube secret-store generation', () => {
90
97
 
91
98
  const serialized = JSON.stringify(result.files);
92
99
  expect(serialized).toContain('auth/oauth/google');
93
- expect(serialized).not.toContain('clientSecret');
94
- expect(serialized).not.toContain('privateKey');
100
+ expect(serialized).not.toContain('sentinel-client-secret-value');
101
+ expect(serialized).not.toContain('sentinel-private-key-value');
95
102
  });
96
103
 
97
104
  test('rejects unknown secret-store providers', () => {
@@ -102,6 +109,7 @@ describe('minikube secret-store generation', () => {
102
109
  provider: 'aws-secrets-manager',
103
110
  },
104
111
  }),
112
+ { appManifest: createAppManifest('vault-app') },
105
113
  ),
106
114
  ).toThrow('Unsupported secret-store provider for minikube adapter');
107
115
  });
@@ -19,7 +19,7 @@ export function generateSupabaseVaultSecretStoreArtifacts(args: {
19
19
  files: [
20
20
  {
21
21
  path: SUPABASE_VAULT_MIGRATION_PATH,
22
- content: `${SUPABASE_VAULT_MIGRATION_SQL.trim()}\n`,
22
+ content: getCurrentSupabaseVaultMigrationSql(),
23
23
  },
24
24
  {
25
25
  path: 'infra/minikube/secrets/supabase-vault.md',
@@ -27,14 +27,60 @@ export function generateSupabaseVaultSecretStoreArtifacts(args: {
27
27
  },
28
28
  ],
29
29
  resources: [],
30
- envEntries: [
31
- `SECRET_STORE_PROVIDER=${SUPABASE_VAULT_SECRET_STORE_PROVIDER}`,
32
- 'SUPABASE_LOCAL_ENABLED=true',
33
- ],
30
+ providerLifecycle: [],
31
+ envEntries: [`SECRET_STORE_PROVIDER=${SUPABASE_VAULT_SECRET_STORE_PROVIDER}`],
34
32
  warnings: [],
35
33
  };
36
34
  }
37
35
 
36
+ function getCurrentSupabaseVaultMigrationSql(): string {
37
+ return `${SUPABASE_VAULT_MIGRATION_SQL.trim()
38
+ .replace(
39
+ 'create extension if not exists supabase_vault with schema extensions;',
40
+ () => `create schema if not exists vault;
41
+ do $$
42
+ begin
43
+ create extension if not exists supabase_vault with schema vault;
44
+ exception
45
+ when insufficient_privilege then
46
+ create table if not exists vault.secrets (
47
+ id uuid primary key default gen_random_uuid(),
48
+ secret text not null,
49
+ name text,
50
+ description text,
51
+ created_at timestamptz not null default now()
52
+ );
53
+
54
+ create or replace function vault.create_secret(
55
+ secret text,
56
+ name text default null,
57
+ description text default null
58
+ )
59
+ returns uuid
60
+ language plpgsql
61
+ security definer
62
+ set search_path = vault, public
63
+ as $vault$
64
+ declare
65
+ secret_id uuid;
66
+ begin
67
+ insert into vault.secrets (secret, name, description)
68
+ values (secret, name, description)
69
+ returning id into secret_id;
70
+
71
+ return secret_id;
72
+ end;
73
+ $vault$;
74
+
75
+ create or replace view vault.decrypted_secrets as
76
+ select id, secret as decrypted_secret
77
+ from vault.secrets;
78
+ end
79
+ $$;`,
80
+ )
81
+ .trim()}\n`;
82
+ }
83
+
38
84
  function validateOAuthSecretReferences(manifest: InfraManifestInput): void {
39
85
  const oauth = manifest.auth?.oauth;
40
86
  if (!oauth?.enabled) return;
@@ -68,7 +114,8 @@ The immutable migration at:
68
114
  ${SUPABASE_VAULT_MIGRATION_PATH}
69
115
  \`\`\`
70
116
 
71
- is applied by the existing local Supabase migration lifecycle during \`scripts/up.sh\`.
117
+ is applied by the Kubernetes-owned Supabase migration lifecycle during \`scripts/up.sh\`
118
+ with \`supabase migration up --db-url "$SUPABASE_DB_URL"\`.
72
119
 
73
120
  ## Security boundary
74
121
 
@@ -25,11 +25,6 @@ export function generateSupabaseStorageArtifacts(args: {
25
25
  envEntries.push(
26
26
  '',
27
27
  '# Supabase provider connection details',
28
- 'SUPABASE_SECRET_SYNC_ENABLED=true',
29
- 'SUPABASE_URL=',
30
- 'SUPABASE_ANON_KEY=',
31
- 'SUPABASE_SERVICE_ROLE_KEY=',
32
- 'SUPABASE_JWT_SECRET=',
33
28
  'EXPO_PUBLIC_SUPABASE_URL=',
34
29
  'EXPO_PUBLIC_SUPABASE_ANON_KEY=',
35
30
  );
@@ -58,6 +53,7 @@ export function generateSupabaseStorageArtifacts(args: {
58
53
  `${resourceRoot}/supabase-storage.configmap.yaml`,
59
54
  `${resourceRoot}/app-runtime-storage.env.configmap.yaml`,
60
55
  ],
56
+ providerLifecycle: [],
61
57
  envEntries,
62
58
  warnings: [
63
59
  `Storage buckets are configured but not created automatically yet. Ensure buckets exist in Supabase Storage: ${bucketsCsv}.`,
package/src/apis.test.ts CHANGED
@@ -2,6 +2,7 @@ import type { AppManifest } from '@ankhorage/contracts';
2
2
  import { describe, expect, test } from 'bun:test';
3
3
 
4
4
  import { generateInfrastructure } from './index';
5
+ import { createAppManifest } from './testSupport';
5
6
  import type { InfraManifestInput } from './types';
6
7
 
7
8
  describe('API infrastructure generation', () => {
@@ -197,7 +198,7 @@ describe('API infrastructure generation', () => {
197
198
  plugins: [],
198
199
  };
199
200
 
200
- const result = generateInfrastructure(manifest, { namespaceHint: 'cards' });
201
+ const result = generateInfrastructure(manifest, { appManifest: createAppManifest('cards') });
201
202
 
202
203
  expect(result.meta.providers).toContain('legend');
203
204
  expect(result.dependencies).toEqual([