@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.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -1
- package/dist/adapters/minikube/auth/supabase/index.js +14 -39
- package/dist/adapters/minikube/auth/supabase/index.js.map +1 -1
- package/dist/adapters/minikube/auth/supabase/profile.d.ts +0 -1
- package/dist/adapters/minikube/auth/supabase/profile.d.ts.map +1 -1
- package/dist/adapters/minikube/auth/supabase/profile.js +1 -1
- package/dist/adapters/minikube/auth/supabase/profile.js.map +1 -1
- package/dist/adapters/minikube/authz/cerbos/index.d.ts.map +1 -1
- package/dist/adapters/minikube/authz/cerbos/index.js +22 -2
- package/dist/adapters/minikube/authz/cerbos/index.js.map +1 -1
- package/dist/adapters/minikube/base/index.d.ts +5 -2
- package/dist/adapters/minikube/base/index.d.ts.map +1 -1
- package/dist/adapters/minikube/base/index.js +2764 -1841
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/dist/adapters/minikube/contracts.d.ts +25 -0
- package/dist/adapters/minikube/contracts.d.ts.map +1 -1
- package/dist/adapters/minikube/contracts.js +1 -0
- package/dist/adapters/minikube/contracts.js.map +1 -1
- package/dist/adapters/minikube/index.d.ts.map +1 -1
- package/dist/adapters/minikube/index.js +18 -33
- package/dist/adapters/minikube/index.js.map +1 -1
- package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts.map +1 -1
- package/dist/adapters/minikube/secrets/supabase-vault/index.js +49 -6
- package/dist/adapters/minikube/secrets/supabase-vault/index.js.map +1 -1
- package/dist/adapters/minikube/storage/supabase/index.d.ts.map +1 -1
- package/dist/adapters/minikube/storage/supabase/index.js +2 -1
- package/dist/adapters/minikube/storage/supabase/index.js.map +1 -1
- package/package.json +4 -1
- package/src/adapters/minikube/README.md +46 -38
- package/src/adapters/minikube/auth/supabase/index.ts +14 -40
- package/src/adapters/minikube/auth/supabase/profile.ts +1 -1
- package/src/adapters/minikube/authz/cerbos/index.ts +25 -3
- package/src/adapters/minikube/base/index.test.ts +399 -673
- package/src/adapters/minikube/base/index.ts +2969 -1874
- package/src/adapters/minikube/base/twoAppIsolation.integration.test.ts +636 -0
- package/src/adapters/minikube/contracts.ts +29 -0
- package/src/adapters/minikube/index.ts +18 -44
- package/src/adapters/minikube/secrets/index.test.ts +15 -7
- package/src/adapters/minikube/secrets/supabase-vault/index.ts +53 -6
- package/src/adapters/minikube/storage/supabase/index.ts +1 -5
- package/src/apis.test.ts +2 -1
- package/src/index.test.ts +64 -577
- 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
|
|
34
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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).
|
|
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('
|
|
35
|
-
expect(upScript?.content).toContain('
|
|
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('
|
|
94
|
-
expect(serialized).not.toContain('
|
|
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:
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
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, {
|
|
201
|
+
const result = generateInfrastructure(manifest, { appManifest: createAppManifest('cards') });
|
|
201
202
|
|
|
202
203
|
expect(result.meta.providers).toContain('legend');
|
|
203
204
|
expect(result.dependencies).toEqual([
|