@ankhorage/infra 1.0.6 → 2.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 +4 -1
- package/dist/adapters/minikube/auth/index.d.ts.map +1 -1
- package/dist/adapters/minikube/auth/index.js +13 -1
- package/dist/adapters/minikube/auth/index.js.map +1 -1
- package/dist/adapters/minikube/auth/oauthRuntime.d.ts +4 -0
- package/dist/adapters/minikube/auth/oauthRuntime.d.ts.map +1 -0
- package/dist/adapters/minikube/auth/oauthRuntime.js +144 -0
- package/dist/adapters/minikube/auth/oauthRuntime.js.map +1 -0
- package/dist/adapters/minikube/index.d.ts.map +1 -1
- package/dist/adapters/minikube/index.js +15 -6
- package/dist/adapters/minikube/index.js.map +1 -1
- package/dist/authRedirects.d.ts +19 -0
- package/dist/authRedirects.d.ts.map +1 -0
- package/dist/authRedirects.js +80 -0
- package/dist/authRedirects.js.map +1 -0
- package/dist/generatedApiDatabase.d.ts +20 -0
- package/dist/generatedApiDatabase.d.ts.map +1 -0
- package/dist/generatedApiDatabase.js +343 -0
- package/dist/generatedApiDatabase.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/project.d.ts +1 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +3 -0
- package/dist/project.js.map +1 -1
- package/dist/testSupport.d.ts.map +1 -1
- package/dist/testSupport.js +1 -0
- package/dist/testSupport.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 +2 -2
- package/src/adapters/minikube/auth/index.ts +14 -1
- package/src/adapters/minikube/auth/oauthRuntime.ts +169 -0
- package/src/adapters/minikube/auth/supabase/oauthRuntime.test.ts +143 -0
- package/src/adapters/minikube/auth/supabase/providerAssignments.test.ts +36 -0
- package/src/adapters/minikube/index.ts +20 -6
- package/src/authRedirects.test.ts +63 -0
- package/src/authRedirects.ts +113 -0
- package/src/generatedApiDatabase.test.ts +355 -0
- package/src/generatedApiDatabase.ts +558 -0
- package/src/index.ts +14 -2
- package/src/project.ts +3 -1
- package/src/testSupport.ts +1 -0
- package/src/types.ts +1 -1
- package/dist/apiArtifacts.d.ts +0 -8
- package/dist/apiArtifacts.d.ts.map +0 -1
- package/dist/apiArtifacts.js +0 -35
- package/dist/apiArtifacts.js.map +0 -1
- package/dist/apis.d.ts +0 -11
- package/dist/apis.d.ts.map +0 -1
- package/dist/apis.js +0 -354
- package/dist/apis.js.map +0 -1
- package/src/apiArtifacts.test.ts +0 -54
- package/src/apiArtifacts.ts +0 -107
- package/src/apis.test.ts +0 -212
- package/src/apis.ts +0 -534
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACpE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,KAAK,EAAE,SAAS,2BAA2B,EAAE,CAAC;IACvD,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE,SAAS,0BAA0B,EAAE,CAAC;CAC9D;AAED,MAAM,WAAW,+BAA+B;IAC9C;;OAEG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CACzB,WAAW,EACX,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACpE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,KAAK,EAAE,SAAS,2BAA2B,EAAE,CAAC;IACvD,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE,SAAS,0BAA0B,EAAE,CAAC;CAC9D;AAED,MAAM,WAAW,+BAA+B;IAC9C;;OAEG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CACzB,WAAW,EACX,eAAe,GAAG,OAAO,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,CAC9E,CAAC;CACH;AAED,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,+BAA+B,CAAC;CACpD;AAED,MAAM,MAAM,sBAAsB,GAAG,+BAA+B,CAAC;AACrE,MAAM,MAAM,qBAAqB,GAAG,8BAA8B,CAAC;AACnE,MAAM,MAAM,aAAa,GAAG,2BAA2B,CAAC;AACxD,MAAM,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAE3D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,KAAK,oBAAoB,GAAG,WAAW,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,UAAU,CAAC;AAE3F,UAAU,qBAAqB;IAC7B,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;CAC1C,CAAC"}
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AppManifest, InfraManifest } from '@ankhorage/contracts';\n\nexport interface GeneratedInfrastructureFile {\n /**\n * Path relative to the target app root.\n */\n readonly path: string;\n readonly content: string;\n readonly executable?: boolean;\n}\n\nexport interface GeneratedPackageDependency {\n readonly name: string;\n readonly version: string;\n readonly reason: string;\n}\n\nexport interface InfrastructureGenerationMeta {\n readonly target: NonNullable<InfraManifest['deployment']>['target'];\n readonly providers: readonly string[];\n}\n\nexport interface InfrastructureGenerationResult {\n readonly files: readonly GeneratedInfrastructureFile[];\n readonly warnings: readonly string[];\n readonly meta: InfrastructureGenerationMeta;\n readonly dependencies: readonly GeneratedPackageDependency[];\n}\n\nexport interface InfrastructureGenerationOptions {\n /**\n * Optional namespace hint from caller context, such as app slug or project id.\n */\n readonly namespaceHint?: string;\n /**\n * Optional app-level context used by provider/engine-specific generators.\n */\n readonly appManifest?: Pick<\n AppManifest,\n '
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AppManifest, InfraManifest } from '@ankhorage/contracts';\n\nexport interface GeneratedInfrastructureFile {\n /**\n * Path relative to the target app root.\n */\n readonly path: string;\n readonly content: string;\n readonly executable?: boolean;\n}\n\nexport interface GeneratedPackageDependency {\n readonly name: string;\n readonly version: string;\n readonly reason: string;\n}\n\nexport interface InfrastructureGenerationMeta {\n readonly target: NonNullable<InfraManifest['deployment']>['target'];\n readonly providers: readonly string[];\n}\n\nexport interface InfrastructureGenerationResult {\n readonly files: readonly GeneratedInfrastructureFile[];\n readonly warnings: readonly string[];\n readonly meta: InfrastructureGenerationMeta;\n readonly dependencies: readonly GeneratedPackageDependency[];\n}\n\nexport interface InfrastructureGenerationOptions {\n /**\n * Optional namespace hint from caller context, such as app slug or project id.\n */\n readonly namespaceHint?: string;\n /**\n * Optional app-level context used by provider/engine-specific generators.\n */\n readonly appManifest?: Pick<\n AppManifest,\n 'generatedApis' | 'infra' | 'metadata' | 'navigator' | 'screens' | 'settings'\n >;\n}\n\nexport interface InfraGenerationInput {\n /**\n * Infra manifest section from an app manifest.\n */\n readonly manifest: InfraManifestInput;\n /**\n * Optional root used by callers that want to keep output intent alongside generation.\n * The generator itself still returns relative file paths and does not write to disk.\n */\n readonly outputRoot?: string;\n readonly options?: InfrastructureGenerationOptions;\n}\n\nexport type InfraGenerationOptions = InfrastructureGenerationOptions;\nexport type InfraGenerationResult = InfrastructureGenerationResult;\nexport type GeneratedFile = GeneratedInfrastructureFile;\nexport type PackageDependency = GeneratedPackageDependency;\n\nexport interface InfraDiagnostic {\n readonly severity: 'warning';\n readonly message: string;\n}\n\ntype InfraStorageProvider = NonNullable<InfraManifest['storage']>['provider'] | 'supabase';\n\ninterface InfraStorageSpecInput {\n readonly provider: InfraStorageProvider;\n readonly buckets: readonly string[];\n}\n\n/**\n * Infra-local manifest input type.\n *\n * This is intentionally broader than `@ankhorage/contracts` to allow adapters to support\n * additional providers before the shared contracts package officially includes them.\n */\nexport type InfraManifestInput = Omit<InfraManifest, 'storage'> & {\n readonly storage?: InfraStorageSpecInput;\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/infra",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.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": "^4.0.2",
|
|
39
39
|
"@ankhorage/supabase-vault": "^0.2.4"
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { InfraManifestInput } from '../../../types';
|
|
2
2
|
import { emptyMinikubeArtifacts, type MinikubeAdapterArtifacts } from '../contracts';
|
|
3
|
+
import { generateSupabaseOAuthRuntimeArtifacts } from './oauthRuntime';
|
|
3
4
|
import { generateSupabaseAuthArtifacts } from './supabase';
|
|
4
5
|
|
|
5
6
|
export function generateAuthProviderArtifacts(args: {
|
|
@@ -19,5 +20,17 @@ export function generateAuthProviderArtifacts(args: {
|
|
|
19
20
|
);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
const providerArtifacts = generateSupabaseAuthArtifacts({ manifest, namespace });
|
|
24
|
+
const oauthArtifacts = generateSupabaseOAuthRuntimeArtifacts(manifest);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
files: [...providerArtifacts.files, ...oauthArtifacts.files],
|
|
28
|
+
resources: [...providerArtifacts.resources, ...oauthArtifacts.resources],
|
|
29
|
+
providerLifecycle: [
|
|
30
|
+
...providerArtifacts.providerLifecycle,
|
|
31
|
+
...oauthArtifacts.providerLifecycle,
|
|
32
|
+
],
|
|
33
|
+
envEntries: [...providerArtifacts.envEntries, ...oauthArtifacts.envEntries],
|
|
34
|
+
warnings: [...providerArtifacts.warnings, ...oauthArtifacts.warnings],
|
|
35
|
+
};
|
|
23
36
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { getLocalAuthRedirectPatterns, normalizeAuthCallbackRoute } from '../../../authRedirects';
|
|
2
|
+
import type { InfraManifestInput } from '../../../types';
|
|
3
|
+
import type { MinikubeAdapterArtifacts, MinikubeProviderLifecycle } from '../contracts';
|
|
4
|
+
|
|
5
|
+
interface SupabaseOAuthRuntimeModel {
|
|
6
|
+
callbackRoute: string;
|
|
7
|
+
envPrefixes: readonly string[];
|
|
8
|
+
localRedirectPatterns: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function generateSupabaseOAuthRuntimeArtifacts(
|
|
12
|
+
manifest: InfraManifestInput,
|
|
13
|
+
): MinikubeAdapterArtifacts {
|
|
14
|
+
const oauthRuntime = resolveOAuthRuntimeModel(manifest);
|
|
15
|
+
if (!oauthRuntime) {
|
|
16
|
+
return {
|
|
17
|
+
files: [],
|
|
18
|
+
resources: [],
|
|
19
|
+
providerLifecycle: [],
|
|
20
|
+
envEntries: [],
|
|
21
|
+
warnings: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
files: [
|
|
27
|
+
{
|
|
28
|
+
path: 'infra/minikube/auth/oauth-runtime.md',
|
|
29
|
+
content: getOAuthRuntimeGuide(oauthRuntime),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
resources: [],
|
|
33
|
+
providerLifecycle: [getOAuthProviderLifecycle(oauthRuntime)],
|
|
34
|
+
envEntries: [],
|
|
35
|
+
warnings: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveOAuthRuntimeModel(manifest: InfraManifestInput): SupabaseOAuthRuntimeModel | null {
|
|
40
|
+
const oauth = manifest.auth?.oauth;
|
|
41
|
+
if (!oauth?.enabled) return null;
|
|
42
|
+
|
|
43
|
+
const providers = oauth.providers.filter((provider) => provider.enabled !== false);
|
|
44
|
+
if (providers.length === 0) return null;
|
|
45
|
+
|
|
46
|
+
const callbackRoute = normalizeAuthCallbackRoute(oauth.callbackRoute);
|
|
47
|
+
const envPrefixes = providers.map((provider) => {
|
|
48
|
+
const prefix = provider.id
|
|
49
|
+
.trim()
|
|
50
|
+
.toUpperCase()
|
|
51
|
+
.replace(/[^A-Z0-9]+/gu, '_')
|
|
52
|
+
.replace(/^_+|_+$/gu, '');
|
|
53
|
+
if (!prefix) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`OAuth provider "${provider.id}" cannot be mapped to GoTrue environment keys.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return prefix;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
callbackRoute,
|
|
63
|
+
envPrefixes,
|
|
64
|
+
localRedirectPatterns: getLocalAuthRedirectPatterns(callbackRoute),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getOAuthProviderLifecycle(
|
|
69
|
+
oauthRuntime: SupabaseOAuthRuntimeModel,
|
|
70
|
+
): MinikubeProviderLifecycle {
|
|
71
|
+
return {
|
|
72
|
+
id: 'supabase-auth',
|
|
73
|
+
namespace: 'supabase',
|
|
74
|
+
endpoints: [],
|
|
75
|
+
readinessChecks: [
|
|
76
|
+
{
|
|
77
|
+
label: 'GoTrue',
|
|
78
|
+
namespace: 'supabase',
|
|
79
|
+
resource: 'deployment/auth',
|
|
80
|
+
timeoutSeconds: 600,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
migrationCommands: [],
|
|
84
|
+
reconciliationCommands: [
|
|
85
|
+
{
|
|
86
|
+
label: 'OAuth redirect and runtime rollout reconciliation',
|
|
87
|
+
command: getOAuthRuntimeReconciliationCommand(oauthRuntime),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
statusChecks: [
|
|
91
|
+
{
|
|
92
|
+
label: 'OAuth redirect configuration',
|
|
93
|
+
command: getOAuthRuntimeStatusCommand(oauthRuntime),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getOAuthRuntimeReconciliationCommand(oauthRuntime: SupabaseOAuthRuntimeModel): string {
|
|
100
|
+
const localPatterns = oauthRuntime.localRedirectPatterns.join(',');
|
|
101
|
+
const providerRedirectAssignments = oauthRuntime.envPrefixes
|
|
102
|
+
.map((prefix) => `GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI="\${oauth_provider_callback}"`)
|
|
103
|
+
.join(' ');
|
|
104
|
+
const providerExports = oauthRuntime.envPrefixes
|
|
105
|
+
.map(
|
|
106
|
+
(prefix) => `export GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI="\${oauth_provider_callback}"
|
|
107
|
+
write_env_value GOTRUE_EXTERNAL_${prefix}_REDIRECT_URI "\${oauth_provider_callback}"`,
|
|
108
|
+
)
|
|
109
|
+
.join('\n');
|
|
110
|
+
|
|
111
|
+
return `oauth_callback_route='${oauthRuntime.callbackRoute}'
|
|
112
|
+
oauth_callback_path="\${oauth_callback_route#/}"
|
|
113
|
+
oauth_site_url="\${SITE_URL%/}"
|
|
114
|
+
oauth_provider_callback="\${API_EXTERNAL_URL%/}/callback"
|
|
115
|
+
oauth_redirect_allow_list="\${oauth_site_url},\${oauth_site_url}/\${oauth_callback_path},${localPatterns}"
|
|
116
|
+
if [[ -n "\${OAUTH_NATIVE_REDIRECT_URLS:-}" ]]; then
|
|
117
|
+
oauth_redirect_allow_list="\${oauth_redirect_allow_list},\${OAUTH_NATIVE_REDIRECT_URLS}"
|
|
118
|
+
fi
|
|
119
|
+
export ADDITIONAL_REDIRECT_URLS="\${oauth_redirect_allow_list}"
|
|
120
|
+
write_env_value ADDITIONAL_REDIRECT_URLS "\${oauth_redirect_allow_list}"
|
|
121
|
+
${providerExports}
|
|
122
|
+
kubectl --context "\${PROFILE}" -n supabase set env deployment/auth \\
|
|
123
|
+
API_EXTERNAL_URL="\${API_EXTERNAL_URL}" \\
|
|
124
|
+
GOTRUE_SITE_URL="\${SITE_URL}" \\
|
|
125
|
+
GOTRUE_URI_ALLOW_LIST="\${oauth_redirect_allow_list}" \\
|
|
126
|
+
GOTRUE_JWT_ISSUER="\${API_EXTERNAL_URL}" \\
|
|
127
|
+
${providerRedirectAssignments} >/dev/null
|
|
128
|
+
kubectl --context "\${PROFILE}" -n supabase rollout restart deployment/auth >/dev/null
|
|
129
|
+
kubectl --context "\${PROFILE}" -n supabase rollout status deployment/auth --timeout=600s`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getOAuthRuntimeStatusCommand(oauthRuntime: SupabaseOAuthRuntimeModel): string {
|
|
133
|
+
return `oauth_callback_route='${oauthRuntime.callbackRoute}'
|
|
134
|
+
oauth_callback_path="\${oauth_callback_route#/}"
|
|
135
|
+
if [[ -n "\${API_EXTERNAL_URL:-}" ]]; then
|
|
136
|
+
echo "- provider supabase-auth/provider-callback: \${API_EXTERNAL_URL%/}/callback"
|
|
137
|
+
else
|
|
138
|
+
echo "- provider supabase-auth/provider-callback: unavailable"
|
|
139
|
+
fi
|
|
140
|
+
if [[ -n "\${SITE_URL:-}" ]]; then
|
|
141
|
+
echo "- provider supabase-auth/app-callback: \${SITE_URL%/}/\${oauth_callback_path}"
|
|
142
|
+
else
|
|
143
|
+
echo "- provider supabase-auth/app-callback: unavailable"
|
|
144
|
+
fi
|
|
145
|
+
echo "- provider supabase-auth/local-callback-patterns: ${oauthRuntime.localRedirectPatterns.join(',')}"`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getOAuthRuntimeGuide(oauthRuntime: SupabaseOAuthRuntimeModel): string {
|
|
149
|
+
const localPatterns = oauthRuntime.localRedirectPatterns
|
|
150
|
+
.map((pattern) => `- \`${pattern}\``)
|
|
151
|
+
.join('\n');
|
|
152
|
+
|
|
153
|
+
return `# Supabase OAuth Runtime Reconciliation
|
|
154
|
+
|
|
155
|
+
The provider callback is derived from the active project gateway as
|
|
156
|
+
\`\${API_EXTERNAL_URL%/}/callback\`, which resolves to the project-owned
|
|
157
|
+
\`/auth/v1/callback\` endpoint. GoTrue-to-app redirects use the configured callback route
|
|
158
|
+
\`${oauthRuntime.callbackRoute}\`.
|
|
159
|
+
|
|
160
|
+
Local Minikube reconciliation adds only callback-scoped loopback patterns:
|
|
161
|
+
|
|
162
|
+
${localPatterns}
|
|
163
|
+
|
|
164
|
+
Native callback URIs may be supplied through \`OAUTH_NATIVE_REDIRECT_URLS\`. The generated
|
|
165
|
+
provider lifecycle writes only redirect metadata, never OAuth client secrets. It updates the
|
|
166
|
+
GoTrue deployment environment, forces \`deployment/auth\` to restart, and waits up to 600
|
|
167
|
+
seconds for the rollout. A failed rollout stops Infra Up before its success message.
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { InfraManifest } from '@ankhorage/contracts';
|
|
2
|
+
import { describe, expect, test } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
import { generateInfrastructure } from '../../../../index';
|
|
5
|
+
import { createAppManifest } from '../../../../testSupport';
|
|
6
|
+
import { generateSupabaseOAuthRuntimeArtifacts } from '../oauthRuntime';
|
|
7
|
+
|
|
8
|
+
describe('Supabase OAuth runtime reconciliation', () => {
|
|
9
|
+
test('contributes callback-scoped local redirects and a bounded GoTrue restart', () => {
|
|
10
|
+
const artifacts = generateSupabaseOAuthRuntimeArtifacts(createOAuthManifest());
|
|
11
|
+
const [lifecycle] = artifacts.providerLifecycle;
|
|
12
|
+
const command = lifecycle?.reconciliationCommands[0]?.command ?? '';
|
|
13
|
+
const statusCommand = lifecycle?.statusChecks[0]?.command ?? '';
|
|
14
|
+
const guide = getFile(artifacts.files, 'infra/minikube/auth/oauth-runtime.md');
|
|
15
|
+
|
|
16
|
+
expect(lifecycle?.id).toBe('supabase-auth');
|
|
17
|
+
expect(lifecycle?.namespace).toBe('supabase');
|
|
18
|
+
expect(lifecycle?.readinessChecks).toEqual([
|
|
19
|
+
{
|
|
20
|
+
label: 'GoTrue',
|
|
21
|
+
namespace: 'supabase',
|
|
22
|
+
resource: 'deployment/auth',
|
|
23
|
+
timeoutSeconds: 600,
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
expect(command).toContain('oauth_provider_callback="${API_EXTERNAL_URL%/}/callback"');
|
|
27
|
+
expect(command).toContain('http://127.0.0.1:*/auth/callback');
|
|
28
|
+
expect(command).toContain('http://localhost:*/auth/callback');
|
|
29
|
+
expect(command).toContain('OAUTH_NATIVE_REDIRECT_URLS');
|
|
30
|
+
expect(command).toContain('GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="${oauth_provider_callback}"');
|
|
31
|
+
expect(command).toContain('GOTRUE_URI_ALLOW_LIST="${oauth_redirect_allow_list}"');
|
|
32
|
+
expect(command).toContain('rollout restart deployment/auth');
|
|
33
|
+
expect(command).toContain('rollout status deployment/auth --timeout=600s');
|
|
34
|
+
expect(command.indexOf('set env deployment/auth')).toBeLessThan(
|
|
35
|
+
command.indexOf('rollout restart deployment/auth'),
|
|
36
|
+
);
|
|
37
|
+
expect(command.indexOf('rollout restart deployment/auth')).toBeLessThan(
|
|
38
|
+
command.indexOf('rollout status deployment/auth'),
|
|
39
|
+
);
|
|
40
|
+
expect(command).not.toContain('clientSecret');
|
|
41
|
+
expect(command).not.toContain('GOTRUE_EXTERNAL_GOOGLE_SECRET=');
|
|
42
|
+
expect(statusCommand).toContain('provider supabase-auth/provider-callback');
|
|
43
|
+
expect(statusCommand).toContain('provider supabase-auth/app-callback');
|
|
44
|
+
expect(guide).toContain('Supabase OAuth Runtime Reconciliation');
|
|
45
|
+
expect(guide).toContain('A failed rollout stops Infra Up before its success message.');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('integrates reconciliation after generated runtime resources without duplicate namespaces', () => {
|
|
49
|
+
const result = generateInfrastructure(createOAuthManifest(), {
|
|
50
|
+
appManifest: createAppManifest('oauth-app'),
|
|
51
|
+
});
|
|
52
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
53
|
+
const kustomization = getFile(result.files, 'infra/minikube/k8s/kustomization.yaml');
|
|
54
|
+
const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh');
|
|
55
|
+
|
|
56
|
+
expect(kustomization.match(/namespaces\/supabase\.yaml/gu)).toHaveLength(1);
|
|
57
|
+
expect(upScript).toContain(
|
|
58
|
+
'Running provider supabase-auth OAuth redirect and runtime rollout reconciliation.',
|
|
59
|
+
);
|
|
60
|
+
expect(upScript).toContain(
|
|
61
|
+
'kubectl --context "${PROFILE}" -n supabase set env deployment/auth',
|
|
62
|
+
);
|
|
63
|
+
expect(upScript).toContain(
|
|
64
|
+
'kubectl --context "${PROFILE}" -n supabase rollout restart deployment/auth',
|
|
65
|
+
);
|
|
66
|
+
expect(upScript).toContain(
|
|
67
|
+
'kubectl --context "${PROFILE}" -n supabase rollout status deployment/auth --timeout=600s',
|
|
68
|
+
);
|
|
69
|
+
expect(upScript.lastIndexOf('run_provider_reconciliation')).toBeLessThan(
|
|
70
|
+
upScript.lastIndexOf('echo "Minikube infrastructure for \'${PROFILE}\' is running."'),
|
|
71
|
+
);
|
|
72
|
+
expect(upScript.indexOf('credentialsRef auth/oauth/google')).toBeLessThan(
|
|
73
|
+
upScript.lastIndexOf('run_provider_reconciliation'),
|
|
74
|
+
);
|
|
75
|
+
expect(statusScript).toContain('provider supabase-auth/local-callback-patterns');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('keeps canonical port groups distinct for concurrent projects', () => {
|
|
79
|
+
const first = generateInfrastructure(createOAuthManifest(), {
|
|
80
|
+
appManifest: createAppManifest('oauth-one'),
|
|
81
|
+
});
|
|
82
|
+
const second = generateInfrastructure(createOAuthManifest(), {
|
|
83
|
+
appManifest: createAppManifest('oauth-two'),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const firstEnv = getFile(first.files, 'infra/minikube/.env.example');
|
|
87
|
+
const secondEnv = getFile(second.files, 'infra/minikube/.env.example');
|
|
88
|
+
const firstGatewayPort = readEnvValue(firstEnv, 'SUPABASE_GATEWAY_FORWARD_LOCAL_PORT');
|
|
89
|
+
const secondGatewayPort = readEnvValue(secondEnv, 'SUPABASE_GATEWAY_FORWARD_LOCAL_PORT');
|
|
90
|
+
const firstAppPort = readEnvValue(firstEnv, 'APP_PORT_FORWARD_LOCAL_PORT');
|
|
91
|
+
const secondAppPort = readEnvValue(secondEnv, 'APP_PORT_FORWARD_LOCAL_PORT');
|
|
92
|
+
|
|
93
|
+
expect(firstGatewayPort).not.toBe(secondGatewayPort);
|
|
94
|
+
expect(firstAppPort).not.toBe(secondAppPort);
|
|
95
|
+
expect(`http://127.0.0.1:${firstGatewayPort}/auth/v1/callback`).not.toBe(
|
|
96
|
+
`http://127.0.0.1:${secondGatewayPort}/auth/v1/callback`,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function createOAuthManifest(): InfraManifest {
|
|
102
|
+
return {
|
|
103
|
+
deployment: {
|
|
104
|
+
target: 'minikube',
|
|
105
|
+
monitoring: false,
|
|
106
|
+
},
|
|
107
|
+
auth: {
|
|
108
|
+
scope: 'global',
|
|
109
|
+
provider: 'supabase',
|
|
110
|
+
oauth: {
|
|
111
|
+
enabled: true,
|
|
112
|
+
callbackRoute: '/auth/callback',
|
|
113
|
+
providers: [
|
|
114
|
+
{
|
|
115
|
+
id: 'google',
|
|
116
|
+
enabled: true,
|
|
117
|
+
credentialsRef: 'auth/oauth/google',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
database: {
|
|
123
|
+
provider: 'supabase',
|
|
124
|
+
tier: 'dev',
|
|
125
|
+
},
|
|
126
|
+
secretStore: {
|
|
127
|
+
provider: 'supabase-vault',
|
|
128
|
+
},
|
|
129
|
+
plugins: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getFile(files: readonly { path: string; content: string }[], path: string): string {
|
|
134
|
+
const file = files.find((candidate) => candidate.path === path);
|
|
135
|
+
if (!file) throw new Error(`Missing generated file: ${path}`);
|
|
136
|
+
return file.content;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readEnvValue(content: string, key: string): string {
|
|
140
|
+
const line = content.split('\n').find((candidate) => candidate.startsWith(`${key}=`));
|
|
141
|
+
if (!line) throw new Error(`Missing generated env key: ${key}`);
|
|
142
|
+
return line.slice(key.length + 1);
|
|
143
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { InfraManifest } from '@ankhorage/contracts';
|
|
2
|
+
import { expect, test } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
import { generateInfrastructure } from '../../../../index';
|
|
5
|
+
import { createAppManifest } from '../../../../testSupport';
|
|
6
|
+
|
|
7
|
+
test('keeps multiple provider redirect assignments as separate shell arguments', () => {
|
|
8
|
+
const manifest: InfraManifest = {
|
|
9
|
+
deployment: { target: 'minikube', monitoring: false },
|
|
10
|
+
auth: {
|
|
11
|
+
scope: 'global',
|
|
12
|
+
provider: 'supabase',
|
|
13
|
+
oauth: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
callbackRoute: '/auth/callback',
|
|
16
|
+
providers: [
|
|
17
|
+
{ id: 'google', enabled: true, credentialsRef: 'auth/oauth/google' },
|
|
18
|
+
{ id: 'apple', enabled: true, credentialsRef: 'auth/oauth/apple' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
database: { provider: 'supabase', tier: 'dev' },
|
|
23
|
+
secretStore: { provider: 'supabase-vault' },
|
|
24
|
+
plugins: [],
|
|
25
|
+
};
|
|
26
|
+
const result = generateInfrastructure(manifest, {
|
|
27
|
+
appManifest: createAppManifest('multi-oauth'),
|
|
28
|
+
});
|
|
29
|
+
const upScript = result.files.find(
|
|
30
|
+
(file) => file.path === 'infra/minikube/scripts/up.sh',
|
|
31
|
+
)?.content;
|
|
32
|
+
|
|
33
|
+
expect(upScript).toContain('GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="${oauth_provider_callback}"');
|
|
34
|
+
expect(upScript).toContain('GOTRUE_EXTERNAL_APPLE_REDIRECT_URI="${oauth_provider_callback}"');
|
|
35
|
+
expect(upScript).not.toContain('\\ GOTRUE_EXTERNAL_APPLE_REDIRECT_URI');
|
|
36
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateGeneratedApiDatabaseArtifacts } from '../../generatedApiDatabase';
|
|
2
2
|
import type {
|
|
3
3
|
GeneratedPackageDependency,
|
|
4
4
|
InfraManifestInput,
|
|
@@ -12,6 +12,7 @@ import { generateSecretStoreArtifacts } from './secrets';
|
|
|
12
12
|
import { generateStorageArtifacts } from './storage';
|
|
13
13
|
|
|
14
14
|
const CANONICAL_PROJECT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
15
|
+
const BUILT_IN_NAMESPACES = new Set([APP_NAMESPACE, 'supabase']);
|
|
15
16
|
|
|
16
17
|
export function generateMinikubeInfra(
|
|
17
18
|
manifest: InfraManifestInput,
|
|
@@ -40,10 +41,21 @@ export function generateMinikubeInfra(
|
|
|
40
41
|
});
|
|
41
42
|
const storageArtifacts = generateStorageArtifacts({ manifest, namespace });
|
|
42
43
|
const secretStoreArtifacts = generateSecretStoreArtifacts({ manifest, namespace });
|
|
43
|
-
const
|
|
44
|
-
|
|
44
|
+
const generatedApiDatabaseArtifacts = generateGeneratedApiDatabaseArtifacts({
|
|
45
|
+
generatedApis: options.appManifest?.generatedApis,
|
|
45
46
|
databaseProvider: manifest.database?.provider,
|
|
46
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
|
+
}
|
|
47
59
|
|
|
48
60
|
const extraResources = unique([
|
|
49
61
|
...authArtifacts.resources,
|
|
@@ -59,7 +71,7 @@ export function generateMinikubeInfra(
|
|
|
59
71
|
];
|
|
60
72
|
const providerNamespaces = unique([
|
|
61
73
|
...providerLifecycle.map((contribution) => contribution.namespace),
|
|
62
|
-
]);
|
|
74
|
+
]).filter((providerNamespace) => !BUILT_IN_NAMESPACES.has(providerNamespace));
|
|
63
75
|
const extraEnvEntries = unique([
|
|
64
76
|
...authArtifacts.envEntries,
|
|
65
77
|
...authzArtifacts.envEntries,
|
|
@@ -82,7 +94,9 @@ export function generateMinikubeInfra(
|
|
|
82
94
|
...authzArtifacts.warnings,
|
|
83
95
|
...storageArtifacts.warnings,
|
|
84
96
|
...secretStoreArtifacts.warnings,
|
|
85
|
-
...
|
|
97
|
+
...generatedApiDatabaseArtifacts.diagnostics
|
|
98
|
+
.filter((diagnostic) => diagnostic.severity === 'warning')
|
|
99
|
+
.map((diagnostic) => diagnostic.message),
|
|
86
100
|
]);
|
|
87
101
|
|
|
88
102
|
return {
|
|
@@ -92,7 +106,7 @@ export function generateMinikubeInfra(
|
|
|
92
106
|
...authzArtifacts.files,
|
|
93
107
|
...storageArtifacts.files,
|
|
94
108
|
...secretStoreArtifacts.files,
|
|
95
|
-
...
|
|
109
|
+
...generatedApiDatabaseArtifacts.files,
|
|
96
110
|
],
|
|
97
111
|
warnings,
|
|
98
112
|
meta: {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { getLocalAuthRedirectPatterns, resolveAuthRedirectConfiguration } from './authRedirects';
|
|
4
|
+
|
|
5
|
+
describe('resolveAuthRedirectConfiguration', () => {
|
|
6
|
+
test('derives an exact provider callback and local browser callback policy', () => {
|
|
7
|
+
const config = resolveAuthRedirectConfiguration({
|
|
8
|
+
environment: 'local',
|
|
9
|
+
gatewayOrigin: 'http://127.0.0.1:18081/',
|
|
10
|
+
siteOrigin: 'http://127.0.0.1:18080/',
|
|
11
|
+
callbackRoute: 'auth/callback',
|
|
12
|
+
webOrigins: ['http://localhost:8081'],
|
|
13
|
+
nativeRedirectUris: ['ankh-demo://auth/callback'],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(config.providerCallbackUrl).toBe('http://127.0.0.1:18081/auth/v1/callback');
|
|
17
|
+
expect(config.siteUrl).toBe('http://127.0.0.1:18080');
|
|
18
|
+
expect(config.redirectAllowList).toEqual([
|
|
19
|
+
'http://127.0.0.1:18080',
|
|
20
|
+
'http://localhost:8081',
|
|
21
|
+
'http://127.0.0.1:18080/auth/callback',
|
|
22
|
+
'http://localhost:8081/auth/callback',
|
|
23
|
+
'http://127.0.0.1:*/auth/callback',
|
|
24
|
+
'http://localhost:*/auth/callback',
|
|
25
|
+
'ankh-demo://auth/callback',
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('never leaks local wildcard policy into preview or production', () => {
|
|
30
|
+
for (const environment of ['preview', 'production'] as const) {
|
|
31
|
+
const config = resolveAuthRedirectConfiguration({
|
|
32
|
+
environment,
|
|
33
|
+
gatewayOrigin: 'https://api.example.test',
|
|
34
|
+
siteOrigin: 'https://app.example.test',
|
|
35
|
+
callbackRoute: '/auth/callback',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(config.redirectAllowList).toEqual([
|
|
39
|
+
'https://app.example.test',
|
|
40
|
+
'https://app.example.test/auth/callback',
|
|
41
|
+
]);
|
|
42
|
+
expect(config.serializedRedirectAllowList).not.toContain('localhost');
|
|
43
|
+
expect(config.serializedRedirectAllowList).not.toContain('127.0.0.1');
|
|
44
|
+
expect(config.serializedRedirectAllowList).not.toContain('*');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('normalizes callback routes and rejects unsafe origins', () => {
|
|
49
|
+
expect(getLocalAuthRedirectPatterns('//auth//callback')).toEqual([
|
|
50
|
+
'http://127.0.0.1:*/auth/callback',
|
|
51
|
+
'http://localhost:*/auth/callback',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
expect(() =>
|
|
55
|
+
resolveAuthRedirectConfiguration({
|
|
56
|
+
environment: 'production',
|
|
57
|
+
gatewayOrigin: 'https://user:secret@example.test',
|
|
58
|
+
siteOrigin: 'https://app.example.test',
|
|
59
|
+
callbackRoute: '/auth/callback',
|
|
60
|
+
}),
|
|
61
|
+
).toThrow('gatewayOrigin must be a canonical origin');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export type AuthRedirectEnvironment = 'local' | 'preview' | 'production';
|
|
2
|
+
|
|
3
|
+
export interface ResolveAuthRedirectConfigurationInput {
|
|
4
|
+
environment: AuthRedirectEnvironment;
|
|
5
|
+
gatewayOrigin: string;
|
|
6
|
+
siteOrigin: string;
|
|
7
|
+
callbackRoute: string;
|
|
8
|
+
webOrigins?: readonly string[];
|
|
9
|
+
nativeRedirectUris?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AuthRedirectConfiguration {
|
|
13
|
+
providerCallbackUrl: string;
|
|
14
|
+
siteUrl: string;
|
|
15
|
+
redirectAllowList: readonly string[];
|
|
16
|
+
serializedRedirectAllowList: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveAuthRedirectConfiguration(
|
|
20
|
+
input: ResolveAuthRedirectConfigurationInput,
|
|
21
|
+
): AuthRedirectConfiguration {
|
|
22
|
+
const gatewayOrigin = normalizeWebOrigin(input.gatewayOrigin, 'gatewayOrigin');
|
|
23
|
+
const siteUrl = normalizeWebOrigin(input.siteOrigin, 'siteOrigin');
|
|
24
|
+
const callbackRoute = normalizeCallbackRoute(input.callbackRoute);
|
|
25
|
+
const webOrigins = unique([
|
|
26
|
+
siteUrl,
|
|
27
|
+
...(input.webOrigins ?? []).map((origin) => normalizeWebOrigin(origin, 'webOrigins')),
|
|
28
|
+
]);
|
|
29
|
+
const nativeRedirectUris = unique(
|
|
30
|
+
(input.nativeRedirectUris ?? []).map((uri) => normalizeNativeRedirectUri(uri)),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const redirectAllowList = unique([
|
|
34
|
+
...webOrigins,
|
|
35
|
+
...webOrigins.map((origin) => `${origin}${callbackRoute}`),
|
|
36
|
+
...(input.environment === 'local' ? getLocalAuthRedirectPatterns(callbackRoute) : []),
|
|
37
|
+
...nativeRedirectUris,
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
providerCallbackUrl: `${gatewayOrigin}/auth/v1/callback`,
|
|
42
|
+
siteUrl,
|
|
43
|
+
redirectAllowList,
|
|
44
|
+
serializedRedirectAllowList: redirectAllowList.join(','),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getLocalAuthRedirectPatterns(callbackRoute: string): readonly string[] {
|
|
49
|
+
const normalizedRoute = normalizeCallbackRoute(callbackRoute);
|
|
50
|
+
return [`http://127.0.0.1:*${normalizedRoute}`, `http://localhost:*${normalizedRoute}`];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeAuthCallbackRoute(callbackRoute: string): string {
|
|
54
|
+
return normalizeCallbackRoute(callbackRoute);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeCallbackRoute(callbackRoute: string): string {
|
|
58
|
+
const trimmed = callbackRoute.trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
throw new Error('Auth callback route must not be empty.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
64
|
+
const normalized = withLeadingSlash.replace(/\/{2,}/gu, '/');
|
|
65
|
+
if (normalized.includes('?') || normalized.includes('#')) {
|
|
66
|
+
throw new Error('Auth callback route must not include a query string or fragment.');
|
|
67
|
+
}
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeWebOrigin(value: string, field: string): string {
|
|
72
|
+
const parsed = parseUrl(value, field);
|
|
73
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
74
|
+
throw new Error(`${field} must use http or https.`);
|
|
75
|
+
}
|
|
76
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
77
|
+
throw new Error(`${field} must be a canonical origin without credentials, query, or fragment.`);
|
|
78
|
+
}
|
|
79
|
+
if (parsed.pathname !== '/' && parsed.pathname !== '') {
|
|
80
|
+
throw new Error(`${field} must not include a path.`);
|
|
81
|
+
}
|
|
82
|
+
return parsed.origin;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeNativeRedirectUri(value: string): string {
|
|
86
|
+
const parsed = parseUrl(value, 'nativeRedirectUris');
|
|
87
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
88
|
+
throw new Error('nativeRedirectUris must use a non-HTTP application scheme.');
|
|
89
|
+
}
|
|
90
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'nativeRedirectUris must not include credentials, query strings, or fragments.',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return parsed.toString();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseUrl(value: string, field: string): URL {
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (!trimmed) {
|
|
101
|
+
throw new Error(`${field} must not contain empty values.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
return new URL(trimmed);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error(`${field} contains an invalid URL.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function unique(values: readonly string[]): string[] {
|
|
112
|
+
return [...new Set(values)];
|
|
113
|
+
}
|