@ankhorage/infra 3.2.0 → 4.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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +14 -16
  3. package/dist/adapters/minikube/auth/index.d.ts +2 -1
  4. package/dist/adapters/minikube/auth/index.d.ts.map +1 -1
  5. package/dist/adapters/minikube/auth/index.js +6 -3
  6. package/dist/adapters/minikube/auth/index.js.map +1 -1
  7. package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.d.ts +7 -0
  8. package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.d.ts.map +1 -0
  9. package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.js +104 -0
  10. package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.js.map +1 -0
  11. package/dist/adapters/minikube/index.d.ts.map +1 -1
  12. package/dist/adapters/minikube/index.js +7 -17
  13. package/dist/adapters/minikube/index.js.map +1 -1
  14. package/dist/authTargetRedirects.d.ts +12 -0
  15. package/dist/authTargetRedirects.d.ts.map +1 -0
  16. package/dist/authTargetRedirects.js +32 -0
  17. package/dist/authTargetRedirects.js.map +1 -0
  18. package/dist/index.d.ts +0 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/infraValidation.d.ts +1 -0
  23. package/dist/infraValidation.d.ts.map +1 -1
  24. package/dist/infraValidation.js +6 -1
  25. package/dist/infraValidation.js.map +1 -1
  26. package/dist/types.d.ts +1 -1
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/types.js.map +1 -1
  29. package/package.json +7 -7
  30. package/src/adapters/minikube/auth/index.ts +8 -4
  31. package/src/adapters/minikube/auth/targetAwareOAuthRuntime.test.ts +83 -0
  32. package/src/adapters/minikube/auth/targetAwareOAuthRuntime.ts +136 -0
  33. package/src/adapters/minikube/index.ts +7 -22
  34. package/src/apiInfrastructure.test.ts +69 -0
  35. package/src/authTargetRedirects.test.ts +62 -0
  36. package/src/authTargetRedirects.ts +46 -0
  37. package/src/generatedApiLedgerCleanup.test.ts +72 -0
  38. package/src/index.ts +0 -6
  39. package/src/infraApiValidation.test.ts +42 -0
  40. package/src/infraValidation.ts +10 -1
  41. package/src/types.ts +1 -1
  42. package/dist/generatedApiDatabase.d.ts +0 -20
  43. package/dist/generatedApiDatabase.d.ts.map +0 -1
  44. package/dist/generatedApiDatabase.js +0 -343
  45. package/dist/generatedApiDatabase.js.map +0 -1
  46. package/src/generatedApiDatabase.test.ts +0 -355
  47. package/src/generatedApiDatabase.ts +0 -558
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/infra",
3
3
  "type": "module",
4
- "version": "3.2.0",
4
+ "version": "4.0.0",
5
5
  "description": "Project-aware infrastructure generation library and executable Ankh provider for Ankhorage.",
6
6
  "homepage": "https://github.com/ankhorage/infra#readme",
7
7
  "bugs": {
@@ -35,7 +35,7 @@
35
35
  "openapi"
36
36
  ],
37
37
  "dependencies": {
38
- "@ankhorage/contracts": "^7.2.0",
38
+ "@ankhorage/contracts": "^8.0.0",
39
39
  "@ankhorage/supabase-vault": "^0.2.4"
40
40
  },
41
41
  "files": [
@@ -106,13 +106,13 @@
106
106
  "version-packages": "changeset version"
107
107
  },
108
108
  "devDependencies": {
109
- "@ankhorage/ankh": "^0.4.0",
110
- "@ankhorage/devtools": "^1.0.6",
111
- "@ankhorage/paradox": "^0.1.10",
109
+ "@ankhorage/ankh": "^0.8.1",
110
+ "@ankhorage/devtools": "^1.2.1",
111
+ "@ankhorage/paradox": "^0.1.21",
112
112
  "@changesets/cli": "^2.31.0",
113
- "@types/bun": "^1.3.13",
113
+ "@types/bun": "^1.3.14",
114
114
  "@types/node": "^25.6.0",
115
115
  "typescript": "^5.9.3"
116
116
  },
117
- "packageManager": "bun@1.3.13"
117
+ "packageManager": "bun@1.3.14"
118
118
  }
@@ -1,13 +1,14 @@
1
- import type { InfraManifestInput } from '../../../types';
1
+ import type { InfraManifestInput, InfrastructureGenerationOptions } from '../../../types';
2
2
  import { emptyMinikubeArtifacts, type MinikubeAdapterArtifacts } from '../contracts';
3
- import { generateSupabaseOAuthRuntimeArtifacts } from './oauthRuntime';
4
3
  import { generateSupabaseAuthArtifacts } from './supabase';
4
+ import { generateTargetAwareSupabaseOAuthRuntimeArtifacts } from './targetAwareOAuthRuntime';
5
5
 
6
6
  export function generateAuthProviderArtifacts(args: {
7
+ appManifest: InfrastructureGenerationOptions['appManifest'];
7
8
  manifest: InfraManifestInput;
8
9
  namespace: string;
9
10
  }): MinikubeAdapterArtifacts {
10
- const { manifest, namespace } = args;
11
+ const { appManifest, manifest, namespace } = args;
11
12
  const { auth } = manifest;
12
13
 
13
14
  if (!auth) {
@@ -21,7 +22,10 @@ export function generateAuthProviderArtifacts(args: {
21
22
  }
22
23
 
23
24
  const providerArtifacts = generateSupabaseAuthArtifacts({ manifest, namespace });
24
- const oauthArtifacts = generateSupabaseOAuthRuntimeArtifacts(manifest);
25
+ const oauthArtifacts = generateTargetAwareSupabaseOAuthRuntimeArtifacts({
26
+ appManifest,
27
+ manifest,
28
+ });
25
29
 
26
30
  return {
27
31
  files: [...providerArtifacts.files, ...oauthArtifacts.files],
@@ -0,0 +1,83 @@
1
+ import type { InfraManifest } from '@ankhorage/contracts';
2
+ import { describe, expect, test } from 'bun:test';
3
+
4
+ import { createAppManifest } from '../../../testSupport';
5
+ import { generateTargetAwareSupabaseOAuthRuntimeArtifacts } from './targetAwareOAuthRuntime';
6
+
7
+ describe('target-aware Supabase OAuth runtime', () => {
8
+ test('removes the legacy native redirect environment source', () => {
9
+ const artifacts = generateTargetAwareSupabaseOAuthRuntimeArtifacts({
10
+ appManifest: undefined,
11
+ manifest: createOAuthManifest(),
12
+ });
13
+ const [lifecycle] = artifacts.providerLifecycle;
14
+ const command = lifecycle?.reconciliationCommands[0]?.command ?? '';
15
+
16
+ expect(command).toContain('http://127.0.0.1:*/auth/callback');
17
+ expect(command).toContain('http://localhost:*/auth/callback');
18
+ expect(command).not.toContain('OAUTH_NATIVE_REDIRECT_URLS');
19
+ expect(artifacts.warnings).toEqual([
20
+ 'OAuth deploy.targets is missing; Infra keeps Web-local redirects only until canonical targets are persisted.',
21
+ ]);
22
+ });
23
+
24
+ test('emits only the enabled native callback for a native-only app', () => {
25
+ const appManifest = {
26
+ ...createAppManifest('native-oauth'),
27
+ deploy: {
28
+ targets: {
29
+ web: { enabled: false },
30
+ android: {
31
+ enabled: true,
32
+ package: 'com.ankh.native',
33
+ scheme: 'ankh-native',
34
+ },
35
+ ios: {
36
+ enabled: false,
37
+ bundleIdentifier: 'com.ankh.native',
38
+ },
39
+ },
40
+ },
41
+ };
42
+ const artifacts = generateTargetAwareSupabaseOAuthRuntimeArtifacts({
43
+ appManifest,
44
+ manifest: createOAuthManifest(),
45
+ });
46
+ const [lifecycle] = artifacts.providerLifecycle;
47
+ const command = lifecycle?.reconciliationCommands[0]?.command ?? '';
48
+ const status = lifecycle?.statusChecks[0]?.command ?? '';
49
+
50
+ expect(command).toContain("oauth_redirect_allow_list='ankh-native://auth/callback'");
51
+ expect(command).not.toContain('oauth_site_url');
52
+ expect(command).not.toContain('localhost:*');
53
+ expect(command).not.toContain('127.0.0.1:*');
54
+ expect(command).not.toContain('OAUTH_NATIVE_REDIRECT_URLS');
55
+ expect(status).toContain('app-callback: disabled (web target)');
56
+ expect(status).toContain('native-callbacks: ankh-native://auth/callback');
57
+ expect(artifacts.warnings).toEqual([]);
58
+ });
59
+ });
60
+
61
+ function createOAuthManifest(): InfraManifest {
62
+ return {
63
+ deployment: { target: 'minikube', monitoring: false },
64
+ auth: {
65
+ scope: 'global',
66
+ provider: 'supabase',
67
+ oauth: {
68
+ enabled: true,
69
+ callbackRoute: '/auth/callback',
70
+ providers: [
71
+ {
72
+ id: 'google',
73
+ enabled: true,
74
+ credentialsRef: 'auth/oauth/google',
75
+ },
76
+ ],
77
+ },
78
+ },
79
+ database: { provider: 'supabase', tier: 'dev' },
80
+ secretStore: { provider: 'supabase-vault' },
81
+ modules: [],
82
+ };
83
+ }
@@ -0,0 +1,136 @@
1
+ import { getLocalAuthRedirectPatterns, normalizeAuthCallbackRoute } from '../../../authRedirects';
2
+ import { resolveAuthTargetRedirectModel } from '../../../authTargetRedirects';
3
+ import type { InfraManifestInput, InfrastructureGenerationOptions } from '../../../types';
4
+ import type { MinikubeAdapterArtifacts, MinikubeProviderLifecycle } from '../contracts';
5
+ import { generateSupabaseOAuthRuntimeArtifacts } from './oauthRuntime';
6
+
7
+ interface RedirectPolicy {
8
+ readonly callbackRoute: string;
9
+ readonly localPatterns: readonly string[];
10
+ readonly nativeRedirectUris: readonly string[];
11
+ readonly webEnabled: boolean;
12
+ }
13
+
14
+ export function generateTargetAwareSupabaseOAuthRuntimeArtifacts(args: {
15
+ readonly appManifest: InfrastructureGenerationOptions['appManifest'];
16
+ readonly manifest: InfraManifestInput;
17
+ }): MinikubeAdapterArtifacts {
18
+ const baseArtifacts = generateSupabaseOAuthRuntimeArtifacts(args.manifest);
19
+ if (!baseArtifacts.providerLifecycle.some((lifecycle) => lifecycle.id === 'supabase-auth')) {
20
+ return baseArtifacts;
21
+ }
22
+
23
+ const callbackRoute = normalizeAuthCallbackRoute(args.manifest.auth?.oauth?.callbackRoute ?? '');
24
+ const targets = resolveAuthTargetRedirectModel({
25
+ callbackRoute,
26
+ targets: args.appManifest?.deploy?.targets,
27
+ });
28
+ const policy: RedirectPolicy = {
29
+ callbackRoute,
30
+ localPatterns: targets.webEnabled ? getLocalAuthRedirectPatterns(callbackRoute) : [],
31
+ nativeRedirectUris: targets.nativeRedirectUris,
32
+ webEnabled: targets.webEnabled,
33
+ };
34
+
35
+ return {
36
+ ...baseArtifacts,
37
+ files: baseArtifacts.files.map((file) =>
38
+ file.path === 'infra/minikube/auth/oauth-runtime.md'
39
+ ? { ...file, content: getTargetAwareGuide(policy) }
40
+ : file,
41
+ ),
42
+ providerLifecycle: baseArtifacts.providerLifecycle.map((lifecycle) =>
43
+ lifecycle.id === 'supabase-auth' ? applyRedirectPolicy(lifecycle, policy) : lifecycle,
44
+ ),
45
+ warnings: [...baseArtifacts.warnings, ...targets.warnings],
46
+ };
47
+ }
48
+
49
+ function applyRedirectPolicy(
50
+ lifecycle: MinikubeProviderLifecycle,
51
+ policy: RedirectPolicy,
52
+ ): MinikubeProviderLifecycle {
53
+ return {
54
+ ...lifecycle,
55
+ reconciliationCommands: lifecycle.reconciliationCommands.map((entry) => ({
56
+ ...entry,
57
+ command: rewriteReconciliationCommand(entry.command, policy),
58
+ })),
59
+ statusChecks: lifecycle.statusChecks.map((entry) => ({
60
+ ...entry,
61
+ command: getTargetAwareStatusCommand(policy),
62
+ })),
63
+ };
64
+ }
65
+
66
+ function rewriteReconciliationCommand(command: string, policy: RedirectPolicy): string {
67
+ const legacyPatterns = getLocalAuthRedirectPatterns(policy.callbackRoute).join(',');
68
+ const legacyPolicy = `oauth_redirect_allow_list="\${oauth_site_url},\${oauth_site_url}/\${oauth_callback_path},${legacyPatterns}"
69
+ if [[ -n "\${OAUTH_NATIVE_REDIRECT_URLS:-}" ]]; then
70
+ oauth_redirect_allow_list="\${oauth_redirect_allow_list},\${OAUTH_NATIVE_REDIRECT_URLS}"
71
+ fi`;
72
+ const staticRedirects = [...policy.localPatterns, ...policy.nativeRedirectUris].join(',');
73
+ const suffix = staticRedirects ? `,${staticRedirects}` : '';
74
+ const canonicalPolicy = policy.webEnabled
75
+ ? `oauth_redirect_allow_list="\${oauth_site_url},\${oauth_site_url}/\${oauth_callback_path}${suffix}"`
76
+ : `oauth_redirect_allow_list='${staticRedirects}'`;
77
+ let rewritten = replaceOnce(command, legacyPolicy, canonicalPolicy);
78
+
79
+ if (!policy.webEnabled) {
80
+ rewritten = replaceOnce(rewritten, 'oauth_site_url="${SITE_URL%/}"\n', '');
81
+ }
82
+
83
+ return rewritten;
84
+ }
85
+
86
+ function getTargetAwareStatusCommand(policy: RedirectPolicy): string {
87
+ const appCallback = policy.webEnabled
88
+ ? `if [[ -n "\${SITE_URL:-}" ]]; then
89
+ echo "- provider supabase-auth/app-callback: \${SITE_URL%/}/\${oauth_callback_path}"
90
+ else
91
+ echo "- provider supabase-auth/app-callback: unavailable"
92
+ fi`
93
+ : 'echo "- provider supabase-auth/app-callback: disabled (web target)"';
94
+ const localPatterns = policy.localPatterns.join(',') || 'none';
95
+ const nativeRedirects = policy.nativeRedirectUris.join(',') || 'none';
96
+
97
+ return `oauth_callback_route='${policy.callbackRoute}'
98
+ oauth_callback_path="\${oauth_callback_route#/}"
99
+ if [[ -n "\${API_EXTERNAL_URL:-}" ]]; then
100
+ echo "- provider supabase-auth/provider-callback: \${API_EXTERNAL_URL%/}/callback"
101
+ else
102
+ echo "- provider supabase-auth/provider-callback: unavailable"
103
+ fi
104
+ ${appCallback}
105
+ echo "- provider supabase-auth/local-callback-patterns: ${localPatterns}"
106
+ echo "- provider supabase-auth/native-callbacks: ${nativeRedirects}"`;
107
+ }
108
+
109
+ function getTargetAwareGuide(policy: RedirectPolicy): string {
110
+ const nativeRedirects = policy.nativeRedirectUris.length
111
+ ? policy.nativeRedirectUris.map((uri) => `- \`${uri}\``).join('\n')
112
+ : '- none';
113
+
114
+ return `# Supabase OAuth Runtime Reconciliation
115
+
116
+ The provider callback remains project-owned. GoTrue-to-app redirects use the configured callback
117
+ route \`${policy.callbackRoute}\` and are derived from \`AppManifest.deploy.targets\`.
118
+
119
+ - Web target: ${policy.webEnabled ? 'enabled' : 'disabled'}
120
+ - Native callbacks:
121
+ ${nativeRedirects}
122
+
123
+ Local loopback patterns are emitted only for an enabled Web target. Native callbacks come only
124
+ from enabled Android/iOS target schemes; no environment variable or slug-derived identity can add
125
+ a native callback. Provider credential values remain sourced from trusted secret resolution.
126
+ `;
127
+ }
128
+
129
+ function replaceOnce(value: string, search: string, replacement: string): string {
130
+ if (!value.includes(search)) {
131
+ throw new Error(
132
+ 'Supabase OAuth redirect reconciliation template drifted from its canonical form.',
133
+ );
134
+ }
135
+ return value.replace(search, replacement);
136
+ }
@@ -1,4 +1,4 @@
1
- import { generateGeneratedApiDatabaseArtifacts } from '../../generatedApiDatabase';
1
+ import { validateInfraApiSupport } from '../../infraValidation';
2
2
  import type {
3
3
  GeneratedPackageDependency,
4
4
  InfraManifestInput,
@@ -33,7 +33,11 @@ export function generateMinikubeInfra(
33
33
  const appSlug = validateCanonicalAppSlug(options.appManifest?.metadata.slug);
34
34
  const namespace = APP_NAMESPACE;
35
35
 
36
- const authArtifacts = generateAuthProviderArtifacts({ manifest, namespace });
36
+ const authArtifacts = generateAuthProviderArtifacts({
37
+ appManifest: options.appManifest,
38
+ manifest,
39
+ namespace,
40
+ });
37
41
  const authzArtifacts = generateAuthorizationArtifacts({
38
42
  manifest,
39
43
  namespace,
@@ -41,21 +45,6 @@ export function generateMinikubeInfra(
41
45
  });
42
46
  const storageArtifacts = generateStorageArtifacts({ manifest, namespace });
43
47
  const secretStoreArtifacts = generateSecretStoreArtifacts({ manifest, namespace });
44
- const generatedApiDatabaseArtifacts = generateGeneratedApiDatabaseArtifacts({
45
- generatedApis: options.appManifest?.generatedApis,
46
- databaseProvider: manifest.database?.provider,
47
- });
48
- const generatedApiErrors = generatedApiDatabaseArtifacts.diagnostics.filter(
49
- (diagnostic) => diagnostic.severity === 'error',
50
- );
51
-
52
- if (generatedApiErrors.length > 0) {
53
- throw new Error(
54
- `Generated API database definition is invalid:\n${generatedApiErrors
55
- .map((diagnostic) => `- ${diagnostic.path ?? diagnostic.apiId}: ${diagnostic.message}`)
56
- .join('\n')}`,
57
- );
58
- }
59
48
 
60
49
  const extraResources = unique([
61
50
  ...authArtifacts.resources,
@@ -94,9 +83,6 @@ export function generateMinikubeInfra(
94
83
  ...authzArtifacts.warnings,
95
84
  ...storageArtifacts.warnings,
96
85
  ...secretStoreArtifacts.warnings,
97
- ...generatedApiDatabaseArtifacts.diagnostics
98
- .filter((diagnostic) => diagnostic.severity === 'warning')
99
- .map((diagnostic) => diagnostic.message),
100
86
  ]);
101
87
 
102
88
  return {
@@ -106,7 +92,6 @@ export function generateMinikubeInfra(
106
92
  ...authzArtifacts.files,
107
93
  ...storageArtifacts.files,
108
94
  ...secretStoreArtifacts.files,
109
- ...generatedApiDatabaseArtifacts.files,
110
95
  ],
111
96
  warnings,
112
97
  meta: {
@@ -147,7 +132,7 @@ function collectDependencies(manifest: InfraManifestInput): readonly GeneratedPa
147
132
  }
148
133
 
149
134
  function collectWarnings(manifest: InfraManifestInput): string[] {
150
- const warnings: string[] = [];
135
+ const warnings = [...validateInfraApiSupport(manifest)];
151
136
 
152
137
  if (manifest.deployment?.monitoring) {
153
138
  warnings.push(
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { generateInfrastructure } from './index';
4
+ import { createAppManifest } from './testSupport';
5
+ import type { InfraManifestInput } from './types';
6
+
7
+ const DEPLOYMENT = { target: 'minikube', monitoring: false } as const;
8
+
9
+ describe('canonical API infrastructure', () => {
10
+ test('treats external APIs as infrastructure no-ops', () => {
11
+ const manifest: InfraManifestInput = {
12
+ deployment: DEPLOYMENT,
13
+ modules: [],
14
+ apis: [
15
+ {
16
+ id: 'nutrition',
17
+ origin: 'external',
18
+ protocol: 'rest',
19
+ baseUrl: 'https://api.ankhorage.com/v1/nutrition',
20
+ endpoints: {},
21
+ },
22
+ ],
23
+ };
24
+
25
+ const result = generateInfrastructure(manifest, {
26
+ appManifest: createAppManifest('nutrition', manifest),
27
+ });
28
+
29
+ expect(result.warnings).not.toContainEqual(expect.stringContaining('infra.apis.nutrition'));
30
+ expect(listApiArtifactPaths(result.files)).toEqual([]);
31
+ });
32
+
33
+ test('warns for internal APIs without generating partial API infrastructure', () => {
34
+ const manifest: InfraManifestInput = {
35
+ deployment: DEPLOYMENT,
36
+ modules: [],
37
+ apis: [
38
+ {
39
+ id: 'orders',
40
+ origin: 'internal',
41
+ protocol: 'rest',
42
+ basePath: '/api/orders',
43
+ endpoints: {},
44
+ },
45
+ ],
46
+ };
47
+
48
+ const result = generateInfrastructure(manifest, {
49
+ appManifest: createAppManifest('orders', manifest),
50
+ });
51
+
52
+ expect(result.warnings).toContain(
53
+ 'infra.apis.orders has origin "internal", but internal API provisioning is not supported in Phase 1; no API infrastructure will be generated.',
54
+ );
55
+ expect(listApiArtifactPaths(result.files)).toEqual([]);
56
+ });
57
+ });
58
+
59
+ function listApiArtifactPaths(files: readonly { readonly path: string }[]): readonly string[] {
60
+ return files
61
+ .map((file) => file.path)
62
+ .filter(
63
+ (path) =>
64
+ path.includes('generated-api') ||
65
+ path.includes('generated_api') ||
66
+ path.includes('/api/deployment') ||
67
+ path.includes('/api/service'),
68
+ );
69
+ }
@@ -0,0 +1,62 @@
1
+ import type { AppDeployTargets } from '@ankhorage/contracts/deploy';
2
+ import { describe, expect, test } from 'bun:test';
3
+
4
+ import { resolveAuthTargetRedirectModel } from './authTargetRedirects';
5
+
6
+ describe('resolveAuthTargetRedirectModel', () => {
7
+ test('derives enabled native callbacks and deduplicates shared schemes', () => {
8
+ const targets: AppDeployTargets = {
9
+ web: { enabled: true },
10
+ android: {
11
+ enabled: true,
12
+ package: 'com.ankh.demo',
13
+ scheme: 'ankh-demo',
14
+ },
15
+ ios: {
16
+ enabled: true,
17
+ bundleIdentifier: 'com.ankh.demo',
18
+ scheme: 'ankh-demo',
19
+ },
20
+ };
21
+
22
+ expect(resolveAuthTargetRedirectModel({ callbackRoute: '/auth/callback', targets })).toEqual({
23
+ nativeRedirectUris: ['ankh-demo://auth/callback'],
24
+ warnings: [],
25
+ webEnabled: true,
26
+ });
27
+ });
28
+
29
+ test('omits disabled targets and warns instead of guessing missing native schemes', () => {
30
+ const targets: AppDeployTargets = {
31
+ web: { enabled: false },
32
+ android: {
33
+ enabled: false,
34
+ package: 'com.ankh.demo',
35
+ },
36
+ ios: {
37
+ enabled: true,
38
+ bundleIdentifier: 'com.ankh.demo',
39
+ },
40
+ };
41
+
42
+ const result = resolveAuthTargetRedirectModel({ callbackRoute: '/auth/callback', targets });
43
+
44
+ expect(result.nativeRedirectUris).toEqual([]);
45
+ expect(result.webEnabled).toBe(false);
46
+ expect(result.warnings).toEqual([
47
+ 'OAuth ios target is enabled but deploy.targets.ios.scheme is missing; native callback omitted.',
48
+ ]);
49
+ });
50
+
51
+ test('keeps the pre-target manifest path Web-only and explicit', () => {
52
+ expect(
53
+ resolveAuthTargetRedirectModel({ callbackRoute: '/auth/callback', targets: undefined }),
54
+ ).toEqual({
55
+ nativeRedirectUris: [],
56
+ warnings: [
57
+ 'OAuth deploy.targets is missing; Infra keeps Web-local redirects only until canonical targets are persisted.',
58
+ ],
59
+ webEnabled: true,
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,46 @@
1
+ import type { AppDeployTargets } from '@ankhorage/contracts/deploy';
2
+
3
+ interface AuthTargetRedirectModel {
4
+ readonly nativeRedirectUris: readonly string[];
5
+ readonly warnings: readonly string[];
6
+ readonly webEnabled: boolean;
7
+ }
8
+
9
+ export function resolveAuthTargetRedirectModel(args: {
10
+ readonly callbackRoute: string;
11
+ readonly targets: AppDeployTargets | undefined;
12
+ }): AuthTargetRedirectModel {
13
+ if (args.targets === undefined) {
14
+ return {
15
+ nativeRedirectUris: [],
16
+ warnings: [
17
+ 'OAuth deploy.targets is missing; Infra keeps Web-local redirects only until canonical targets are persisted.',
18
+ ],
19
+ webEnabled: true,
20
+ };
21
+ }
22
+
23
+ const callbackPath = args.callbackRoute.replace(/^\/+/u, '');
24
+ const nativeRedirectUris: string[] = [];
25
+ const warnings: string[] = [];
26
+
27
+ for (const [targetId, target] of [
28
+ ['android', args.targets.android],
29
+ ['ios', args.targets.ios],
30
+ ] as const) {
31
+ if (target?.enabled !== true) continue;
32
+ if (target.scheme === undefined) {
33
+ warnings.push(
34
+ `OAuth ${targetId} target is enabled but deploy.targets.${targetId}.scheme is missing; native callback omitted.`,
35
+ );
36
+ continue;
37
+ }
38
+ nativeRedirectUris.push(`${target.scheme}://${callbackPath}`);
39
+ }
40
+
41
+ return {
42
+ nativeRedirectUris: [...new Set(nativeRedirectUris)],
43
+ warnings,
44
+ webEnabled: args.targets.web?.enabled === true,
45
+ };
46
+ }
@@ -0,0 +1,72 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { promises as fs } from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ import { syncProjectInfrastructure } from './projectInfrastructure';
7
+ import { createAppManifest } from './testSupport';
8
+
9
+ const OBSOLETE_FILES = [
10
+ 'infra/minikube/db/generated-api-resources.json',
11
+ 'infra/minikube/db/migrations/001_generated_api_resources.sql',
12
+ ] as const;
13
+
14
+ test('removes obsolete generated API database artifacts tracked by the infra ledger', async () => {
15
+ const projectPath = await fs.mkdtemp(path.join(os.tmpdir(), 'infra-api-cleanup-'));
16
+
17
+ try {
18
+ await writeObsoleteArtifacts(projectPath);
19
+ const manifest = createAppManifest('shop', {
20
+ deployment: { target: 'minikube', monitoring: false },
21
+ modules: [],
22
+ });
23
+ const result = await syncProjectInfrastructure({
24
+ projectId: 'shop',
25
+ projectPath,
26
+ manifest,
27
+ generateInfrastructureImpl: () => ({
28
+ files: [],
29
+ warnings: [],
30
+ meta: { target: 'minikube', providers: [] },
31
+ dependencies: [],
32
+ }),
33
+ });
34
+
35
+ expect(result.removed).toBe(OBSOLETE_FILES.length);
36
+ for (const filePath of OBSOLETE_FILES) {
37
+ expect(await pathExists(path.join(projectPath, filePath))).toBe(false);
38
+ }
39
+ } finally {
40
+ await fs.rm(projectPath, { force: true, recursive: true });
41
+ }
42
+ });
43
+
44
+ async function writeObsoleteArtifacts(projectPath: string): Promise<void> {
45
+ for (const filePath of OBSOLETE_FILES) {
46
+ const absolutePath = path.join(projectPath, filePath);
47
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
48
+ await fs.writeFile(absolutePath, 'obsolete', 'utf8');
49
+ }
50
+
51
+ await fs.mkdir(path.join(projectPath, '.ankh'), { recursive: true });
52
+ await fs.writeFile(
53
+ path.join(projectPath, '.ankh/infra-ledger.json'),
54
+ JSON.stringify({
55
+ schemaVersion: 1,
56
+ generatedAt: new Date().toISOString(),
57
+ target: 'minikube',
58
+ files: OBSOLETE_FILES,
59
+ warnings: [],
60
+ }),
61
+ 'utf8',
62
+ );
63
+ }
64
+
65
+ async function pathExists(filePath: string): Promise<boolean> {
66
+ try {
67
+ await fs.access(filePath);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
package/src/index.ts CHANGED
@@ -14,12 +14,6 @@ export {
14
14
  resolveAuthRedirectConfiguration,
15
15
  type ResolveAuthRedirectConfigurationInput,
16
16
  } from './authRedirects';
17
- export {
18
- type GeneratedApiDatabaseArtifacts,
19
- type GeneratedApiDatabaseDiagnostic,
20
- type GeneratedApiDatabaseDiagnosticCode,
21
- generateGeneratedApiDatabaseArtifacts,
22
- } from './generatedApiDatabase';
23
17
  export {
24
18
  createInfraSecretStoreAdapter,
25
19
  type CreateInfraSecretStoreAdapterInput,
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { validateInfraSupport } from './infraValidation';
4
+ import type { InfraManifestInput } from './types';
5
+
6
+ describe('canonical API support validation', () => {
7
+ test('does not warn for external APIs', () => {
8
+ const manifest: InfraManifestInput = {
9
+ modules: [],
10
+ apis: [
11
+ {
12
+ id: 'nutrition',
13
+ origin: 'external',
14
+ protocol: 'rest',
15
+ baseUrl: 'https://api.ankhorage.com/v1/nutrition',
16
+ endpoints: {},
17
+ },
18
+ ],
19
+ };
20
+
21
+ expect(validateInfraSupport(manifest)).toEqual([]);
22
+ });
23
+
24
+ test('reports every internal API as unsupported without treating it as invalid', () => {
25
+ const manifest: InfraManifestInput = {
26
+ modules: [],
27
+ apis: [
28
+ {
29
+ id: 'orders',
30
+ origin: 'internal',
31
+ protocol: 'rest',
32
+ basePath: '/api/orders',
33
+ endpoints: {},
34
+ },
35
+ ],
36
+ };
37
+
38
+ expect(validateInfraSupport(manifest)).toEqual([
39
+ 'infra.apis.orders has origin "internal", but internal API provisioning is not supported in Phase 1; no API infrastructure will be generated.',
40
+ ]);
41
+ });
42
+ });
@@ -9,7 +9,7 @@ import {
9
9
  import type { InfraManifestInput } from './types.js';
10
10
 
11
11
  export function validateInfraSupport(manifest: InfraManifestInput): readonly string[] {
12
- const warnings: string[] = [];
12
+ const warnings: string[] = [...validateInfraApiSupport(manifest)];
13
13
 
14
14
  const deploymentTarget = manifest.deployment?.target;
15
15
  if (deploymentTarget !== undefined && !isSupported(deploymentTarget, DEPLOYMENT_TARGETS)) {
@@ -69,6 +69,15 @@ export function validateInfraSupport(manifest: InfraManifestInput): readonly str
69
69
  return warnings;
70
70
  }
71
71
 
72
+ export function validateInfraApiSupport(manifest: InfraManifestInput): readonly string[] {
73
+ return (manifest.apis ?? [])
74
+ .filter((api) => api.origin === 'internal')
75
+ .map(
76
+ (api) =>
77
+ `infra.apis.${api.id} has origin "internal", but internal API provisioning is not supported in Phase 1; no API infrastructure will be generated.`,
78
+ );
79
+ }
80
+
72
81
  function warningForUnsupportedValue(args: {
73
82
  readonly path: string;
74
83
  readonly value: string;