@ankhorage/infra 0.3.7 → 0.4.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 (34) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -1
  3. package/dist/adapters/minikube/base/index.d.ts.map +1 -1
  4. package/dist/adapters/minikube/base/index.js +8 -2
  5. package/dist/adapters/minikube/base/index.js.map +1 -1
  6. package/dist/adapters/minikube/index.d.ts.map +1 -1
  7. package/dist/adapters/minikube/index.js +8 -0
  8. package/dist/adapters/minikube/index.js.map +1 -1
  9. package/dist/adapters/minikube/secrets/index.d.ts +7 -0
  10. package/dist/adapters/minikube/secrets/index.d.ts.map +1 -0
  11. package/dist/adapters/minikube/secrets/index.js +13 -0
  12. package/dist/adapters/minikube/secrets/index.js.map +1 -0
  13. package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts +8 -0
  14. package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts.map +1 -0
  15. package/dist/adapters/minikube/secrets/supabase-vault/index.js +74 -0
  16. package/dist/adapters/minikube/secrets/supabase-vault/index.js.map +1 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/secretStore.d.ts +19 -0
  22. package/dist/secretStore.d.ts.map +1 -0
  23. package/dist/secretStore.js +22 -0
  24. package/dist/secretStore.js.map +1 -0
  25. package/package.json +7 -3
  26. package/src/adapters/minikube/base/index.ts +9 -1
  27. package/src/adapters/minikube/index.ts +7 -0
  28. package/src/adapters/minikube/secrets/index.test.ts +103 -0
  29. package/src/adapters/minikube/secrets/index.ts +19 -0
  30. package/src/adapters/minikube/secrets/supabase-vault/index.ts +91 -0
  31. package/src/index.ts +5 -0
  32. package/src/package.test.ts +8 -1
  33. package/src/secretStore.test.ts +76 -0
  34. package/src/secretStore.ts +44 -0
@@ -0,0 +1,91 @@
1
+ import {
2
+ SUPABASE_VAULT_MIGRATION_SQL,
3
+ SUPABASE_VAULT_SECRET_STORE_PROVIDER,
4
+ } from '@ankhorage/supabase-vault';
5
+
6
+ import type { InfraManifestInput } from '../../../../types';
7
+ import type { MinikubeAdapterArtifacts } from '../../contracts';
8
+
9
+ export const SUPABASE_VAULT_MIGRATION_PATH =
10
+ 'infra/minikube/supabase/migrations/202607120001_ankhorage_supabase_vault.sql';
11
+
12
+ export function generateSupabaseVaultSecretStoreArtifacts(args: {
13
+ readonly manifest: InfraManifestInput;
14
+ readonly namespace: string;
15
+ }): MinikubeAdapterArtifacts {
16
+ validateOAuthSecretReferences(args.manifest);
17
+
18
+ return {
19
+ files: [
20
+ {
21
+ path: SUPABASE_VAULT_MIGRATION_PATH,
22
+ content: `${SUPABASE_VAULT_MIGRATION_SQL.trim()}\n`,
23
+ },
24
+ {
25
+ path: 'infra/minikube/secrets/supabase-vault.md',
26
+ content: getRuntimeGuide(args.namespace),
27
+ },
28
+ ],
29
+ resources: [],
30
+ envEntries: [
31
+ `SECRET_STORE_PROVIDER=${SUPABASE_VAULT_SECRET_STORE_PROVIDER}`,
32
+ 'SUPABASE_LOCAL_ENABLED=true',
33
+ ],
34
+ warnings: [],
35
+ };
36
+ }
37
+
38
+ function validateOAuthSecretReferences(manifest: InfraManifestInput): void {
39
+ const oauth = manifest.auth?.oauth;
40
+ if (!oauth?.enabled) return;
41
+
42
+ const enabledProviders = oauth.providers.filter((provider) => provider.enabled !== false);
43
+ if (enabledProviders.length === 0) {
44
+ throw new Error('OAuth is enabled but no OAuth provider is enabled.');
45
+ }
46
+
47
+ for (const provider of enabledProviders) {
48
+ if (!provider.credentialsRef?.trim()) {
49
+ throw new Error(
50
+ `OAuth provider "${provider.id}" is enabled but does not define credentialsRef.`,
51
+ );
52
+ }
53
+ }
54
+ }
55
+
56
+ function getRuntimeGuide(namespace: string): string {
57
+ return `# Supabase Vault secret store
58
+
59
+ This project selects the provider-neutral secret-store backend through:
60
+
61
+ \`\`\`text
62
+ infra.secretStore.provider = ${SUPABASE_VAULT_SECRET_STORE_PROVIDER}
63
+ \`\`\`
64
+
65
+ The immutable migration at:
66
+
67
+ \`\`\`text
68
+ ${SUPABASE_VAULT_MIGRATION_PATH}
69
+ \`\`\`
70
+
71
+ is applied by the existing local Supabase migration lifecycle during \`scripts/up.sh\`.
72
+
73
+ ## Security boundary
74
+
75
+ - Secret values are stored only through Supabase Vault.
76
+ - Public manifests contain logical references such as \`auth/oauth/google\`, never values.
77
+ - Project and environment scope are supplied by trusted server code.
78
+ - The browser may receive secret metadata, but must never receive resolved values or Vault IDs.
79
+ - Bootstrap database access must come from trusted process environment or workload identity.
80
+ - Do not expose service-role keys, provider secrets, or resolved payloads through
81
+ \`NEXT_PUBLIC_*\`, \`EXPO_PUBLIC_*\`, generated YAML, tracked JSON, or app bundles.
82
+
83
+ ## Trusted adapter composition
84
+
85
+ Use \`createInfraSecretStoreAdapter\` from \`@ankhorage/infra\` in server-only code and pass
86
+ a trusted Supabase Vault SQL client. The app manifest chooses the provider; privileged connection
87
+ configuration does not belong in the manifest.
88
+
89
+ Generated Kubernetes namespace: \`${namespace}\`.
90
+ `;
91
+ }
package/src/index.ts CHANGED
@@ -8,6 +8,11 @@ import type {
8
8
 
9
9
  export type { ApiInfrastructureArtifacts } from './apiArtifacts';
10
10
  export { generateApiInfrastructureArtifacts } from './apiArtifacts';
11
+ export {
12
+ createInfraSecretStoreAdapter,
13
+ type CreateInfraSecretStoreAdapterInput,
14
+ type InfraSecretStoreProviders,
15
+ } from './secretStore';
11
16
  export type {
12
17
  GeneratedFile,
13
18
  GeneratedInfrastructureFile,
@@ -8,7 +8,14 @@ describe('package metadata', () => {
8
8
  const expectedAnkhMetadata = {
9
9
  category: 'infra',
10
10
  provider: './dist/cli/index.js',
11
- capabilities: ['infra.validate', 'infra.generate', 'infra.status', 'infra.up', 'infra.down'],
11
+ capabilities: [
12
+ 'infra.validate',
13
+ 'infra.generate',
14
+ 'infra.status',
15
+ 'infra.up',
16
+ 'infra.down',
17
+ 'infra.secret-store',
18
+ ],
12
19
  } as const satisfies AnkhPackageMetadata;
13
20
 
14
21
  expect(packageJson.name).toBe('@ankhorage/infra');
@@ -0,0 +1,76 @@
1
+ import type {
2
+ SupabaseVaultQueryResult,
3
+ SupabaseVaultSqlClient,
4
+ SupabaseVaultSqlExecutor,
5
+ } from '@ankhorage/supabase-vault';
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import { createInfraSecretStoreAdapter } from './secretStore';
9
+
10
+ class FakeSupabaseVaultClient implements SupabaseVaultSqlClient {
11
+ query<TRow extends Record<string, unknown>>(): Promise<SupabaseVaultQueryResult<TRow>> {
12
+ return Promise.resolve({ rows: [] });
13
+ }
14
+
15
+ transaction<TResult>(
16
+ operation: (executor: SupabaseVaultSqlExecutor) => Promise<TResult>,
17
+ ): Promise<TResult> {
18
+ return operation(this);
19
+ }
20
+ }
21
+
22
+ describe('createInfraSecretStoreAdapter', () => {
23
+ test('returns null when the manifest does not select a secret store', () => {
24
+ expect(
25
+ createInfraSecretStoreAdapter({
26
+ manifest: {},
27
+ providers: {},
28
+ }),
29
+ ).toBeNull();
30
+ });
31
+
32
+ test('creates the released Supabase Vault adapter from trusted options', () => {
33
+ const adapter = createInfraSecretStoreAdapter({
34
+ manifest: {
35
+ secretStore: {
36
+ provider: 'supabase-vault',
37
+ },
38
+ },
39
+ providers: {
40
+ supabaseVault: {
41
+ client: new FakeSupabaseVaultClient(),
42
+ },
43
+ },
44
+ });
45
+
46
+ expect(adapter).not.toBeNull();
47
+ expect(typeof adapter?.list).toBe('function');
48
+ expect(typeof adapter?.resolve).toBe('function');
49
+ });
50
+
51
+ test('requires trusted provider bootstrap options', () => {
52
+ expect(() =>
53
+ createInfraSecretStoreAdapter({
54
+ manifest: {
55
+ secretStore: {
56
+ provider: 'supabase-vault',
57
+ },
58
+ },
59
+ providers: {},
60
+ }),
61
+ ).toThrow('requires trusted Supabase Vault adapter options');
62
+ });
63
+
64
+ test('rejects unknown providers instead of silently falling back', () => {
65
+ expect(() =>
66
+ createInfraSecretStoreAdapter({
67
+ manifest: {
68
+ secretStore: {
69
+ provider: 'aws-secrets-manager',
70
+ },
71
+ },
72
+ providers: {},
73
+ }),
74
+ ).toThrow('Unsupported secret-store provider');
75
+ });
76
+ });
@@ -0,0 +1,44 @@
1
+ import type { SecretStoreAdapter } from '@ankhorage/contracts/secrets';
2
+ import {
3
+ createSupabaseVaultAdapter,
4
+ SUPABASE_VAULT_SECRET_STORE_PROVIDER,
5
+ type SupabaseVaultAdapterOptions,
6
+ } from '@ankhorage/supabase-vault';
7
+
8
+ import type { InfraManifestInput } from './types';
9
+
10
+ export interface InfraSecretStoreProviders {
11
+ readonly supabaseVault?: SupabaseVaultAdapterOptions;
12
+ }
13
+
14
+ export interface CreateInfraSecretStoreAdapterInput {
15
+ readonly manifest: Pick<InfraManifestInput, 'secretStore'>;
16
+ readonly providers: InfraSecretStoreProviders;
17
+ }
18
+
19
+ /**
20
+ * Creates the configured server-only secret-store adapter.
21
+ *
22
+ * Bootstrap credentials and trusted clients belong in `providers`; they must never be read from
23
+ * the public app manifest. A missing `infra.secretStore` returns `null` because apps without
24
+ * secret-backed features do not require a secret store.
25
+ */
26
+ export function createInfraSecretStoreAdapter(
27
+ input: CreateInfraSecretStoreAdapterInput,
28
+ ): SecretStoreAdapter | null {
29
+ const provider = input.manifest.secretStore?.provider;
30
+ if (!provider) return null;
31
+
32
+ if (provider === SUPABASE_VAULT_SECRET_STORE_PROVIDER) {
33
+ const options = input.providers.supabaseVault;
34
+ if (!options) {
35
+ throw new Error(
36
+ 'Secret-store provider "supabase-vault" requires trusted Supabase Vault adapter options.',
37
+ );
38
+ }
39
+
40
+ return createSupabaseVaultAdapter(options);
41
+ }
42
+
43
+ throw new Error(`Unsupported secret-store provider: "${provider}".`);
44
+ }