@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.
- package/CHANGELOG.md +12 -0
- package/README.md +14 -16
- package/dist/adapters/minikube/auth/index.d.ts +2 -1
- package/dist/adapters/minikube/auth/index.d.ts.map +1 -1
- package/dist/adapters/minikube/auth/index.js +6 -3
- package/dist/adapters/minikube/auth/index.js.map +1 -1
- package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.d.ts +7 -0
- package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.d.ts.map +1 -0
- package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.js +104 -0
- package/dist/adapters/minikube/auth/targetAwareOAuthRuntime.js.map +1 -0
- package/dist/adapters/minikube/index.d.ts.map +1 -1
- package/dist/adapters/minikube/index.js +7 -17
- package/dist/adapters/minikube/index.js.map +1 -1
- package/dist/authTargetRedirects.d.ts +12 -0
- package/dist/authTargetRedirects.d.ts.map +1 -0
- package/dist/authTargetRedirects.js +32 -0
- package/dist/authTargetRedirects.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/infraValidation.d.ts +1 -0
- package/dist/infraValidation.d.ts.map +1 -1
- package/dist/infraValidation.js +6 -1
- package/dist/infraValidation.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/minikube/auth/index.ts +8 -4
- package/src/adapters/minikube/auth/targetAwareOAuthRuntime.test.ts +83 -0
- package/src/adapters/minikube/auth/targetAwareOAuthRuntime.ts +136 -0
- package/src/adapters/minikube/index.ts +7 -22
- package/src/apiInfrastructure.test.ts +69 -0
- package/src/authTargetRedirects.test.ts +62 -0
- package/src/authTargetRedirects.ts +46 -0
- package/src/generatedApiLedgerCleanup.test.ts +72 -0
- package/src/index.ts +0 -6
- package/src/infraApiValidation.test.ts +42 -0
- package/src/infraValidation.ts +10 -1
- package/src/types.ts +1 -1
- package/dist/generatedApiDatabase.d.ts +0 -20
- package/dist/generatedApiDatabase.d.ts.map +0 -1
- package/dist/generatedApiDatabase.js +0 -343
- package/dist/generatedApiDatabase.js.map +0 -1
- package/src/generatedApiDatabase.test.ts +0 -355
- 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": "
|
|
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": "^
|
|
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.
|
|
110
|
-
"@ankhorage/devtools": "^1.
|
|
111
|
-
"@ankhorage/paradox": "^0.1.
|
|
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.
|
|
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.
|
|
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 =
|
|
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 {
|
|
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({
|
|
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
|
|
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
|
+
});
|
package/src/infraValidation.ts
CHANGED
|
@@ -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;
|