@ankhorage/contracts 14.0.0 → 15.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 +31 -0
- package/README.md +10 -2
- package/dist/appManifest/apis.d.ts +1 -0
- package/dist/appManifest/apis.d.ts.map +1 -1
- package/dist/appManifest/apis.js +17 -14
- package/dist/appManifest/apis.js.map +1 -1
- package/dist/appManifest/bindings.d.ts +3 -0
- package/dist/appManifest/bindings.d.ts.map +1 -1
- package/dist/appManifest/bindings.js +17 -1
- package/dist/appManifest/bindings.js.map +1 -1
- package/dist/appManifest/constants.d.ts +18 -0
- package/dist/appManifest/constants.d.ts.map +1 -0
- package/dist/appManifest/constants.js +18 -0
- package/dist/appManifest/constants.js.map +1 -0
- package/dist/appManifest/data.d.ts +4 -0
- package/dist/appManifest/data.d.ts.map +1 -1
- package/dist/appManifest/data.js +22 -3
- package/dist/appManifest/data.js.map +1 -1
- package/dist/appManifest/dataSources.d.ts +1 -0
- package/dist/appManifest/dataSources.d.ts.map +1 -1
- package/dist/appManifest/dataSources.js +7 -6
- package/dist/appManifest/dataSources.js.map +1 -1
- package/dist/appManifest/deploy.d.ts +1 -0
- package/dist/appManifest/deploy.d.ts.map +1 -1
- package/dist/appManifest/deploy.js +16 -13
- package/dist/appManifest/deploy.js.map +1 -1
- package/dist/appManifest/icon.d.ts.map +1 -1
- package/dist/appManifest/icon.js +2 -1
- package/dist/appManifest/icon.js.map +1 -1
- package/dist/appManifest/isAppManifest.d.ts +4 -0
- package/dist/appManifest/isAppManifest.d.ts.map +1 -0
- package/dist/appManifest/isAppManifest.js +69 -0
- package/dist/appManifest/isAppManifest.js.map +1 -0
- package/dist/appManifest/isAppStateSpec.d.ts +4 -0
- package/dist/appManifest/isAppStateSpec.d.ts.map +1 -0
- package/dist/appManifest/isAppStateSpec.js +9 -0
- package/dist/appManifest/isAppStateSpec.js.map +1 -0
- package/dist/appManifest/isManifestValue.d.ts +3 -0
- package/dist/appManifest/isManifestValue.d.ts.map +1 -0
- package/dist/appManifest/isManifestValue.js +15 -0
- package/dist/appManifest/isManifestValue.js.map +1 -0
- package/dist/appManifest/media.d.ts +1 -0
- package/dist/appManifest/media.d.ts.map +1 -1
- package/dist/appManifest/media.js +10 -8
- package/dist/appManifest/media.js.map +1 -1
- package/dist/appManifest/navigator.d.ts.map +1 -1
- package/dist/appManifest/navigator.js +7 -6
- package/dist/appManifest/navigator.js.map +1 -1
- package/dist/appManifest/navigatorOptions.d.ts +0 -2
- package/dist/appManifest/navigatorOptions.d.ts.map +1 -1
- package/dist/appManifest/navigatorOptions.js +8 -12
- package/dist/appManifest/navigatorOptions.js.map +1 -1
- package/dist/appManifest/parseAppManifest.d.ts +10 -0
- package/dist/appManifest/parseAppManifest.d.ts.map +1 -0
- package/dist/appManifest/parseAppManifest.js +14 -0
- package/dist/appManifest/parseAppManifest.js.map +1 -0
- package/dist/appManifest/screens.d.ts +4 -0
- package/dist/appManifest/screens.d.ts.map +1 -1
- package/dist/appManifest/screens.js +14 -2
- package/dist/appManifest/screens.js.map +1 -1
- package/dist/appManifest.d.ts +3 -18
- package/dist/appManifest.d.ts.map +1 -1
- package/dist/appManifest.js +2 -80
- package/dist/appManifest.js.map +1 -1
- package/dist/auth.d.ts +3 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/deploy.d.ts +0 -9
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +0 -8
- package/dist/deploy.js.map +1 -1
- package/dist/environments.d.ts +4 -0
- package/dist/environments.d.ts.map +1 -0
- package/dist/environments.js +3 -0
- package/dist/environments.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/constants.d.ts +91 -0
- package/dist/infra/constants.d.ts.map +1 -0
- package/dist/infra/constants.js +91 -0
- package/dist/infra/constants.js.map +1 -0
- package/dist/infra/infraFields.d.ts +12 -0
- package/dist/infra/infraFields.d.ts.map +1 -0
- package/dist/infra/infraFields.js +14 -0
- package/dist/infra/infraFields.js.map +1 -0
- package/dist/infra/isInfraAdapterDescriptor.d.ts +4 -0
- package/dist/infra/isInfraAdapterDescriptor.d.ts.map +1 -0
- package/dist/infra/isInfraAdapterDescriptor.js +31 -0
- package/dist/infra/isInfraAdapterDescriptor.js.map +1 -0
- package/dist/infra/isInfraAuthSpec.d.ts +4 -0
- package/dist/infra/isInfraAuthSpec.d.ts.map +1 -0
- package/dist/infra/isInfraAuthSpec.js +75 -0
- package/dist/infra/isInfraAuthSpec.js.map +1 -0
- package/dist/infra/isInfraCredentialRef.d.ts +3 -0
- package/dist/infra/isInfraCredentialRef.d.ts.map +1 -0
- package/dist/infra/isInfraCredentialRef.js +10 -0
- package/dist/infra/isInfraCredentialRef.js.map +1 -0
- package/dist/infra/isInfraDeploymentSpec.d.ts +4 -0
- package/dist/infra/isInfraDeploymentSpec.d.ts.map +1 -0
- package/dist/infra/isInfraDeploymentSpec.js +66 -0
- package/dist/infra/isInfraDeploymentSpec.js.map +1 -0
- package/dist/infra/isInfraEnvironmentSpec.d.ts +4 -0
- package/dist/infra/isInfraEnvironmentSpec.d.ts.map +1 -0
- package/dist/infra/isInfraEnvironmentSpec.js +82 -0
- package/dist/infra/isInfraEnvironmentSpec.js.map +1 -0
- package/dist/infra/isInfraManifest.d.ts +4 -0
- package/dist/infra/isInfraManifest.d.ts.map +1 -0
- package/dist/infra/isInfraManifest.js +23 -0
- package/dist/infra/isInfraManifest.js.map +1 -0
- package/dist/infra/isInfraShape.d.ts +3 -0
- package/dist/infra/isInfraShape.d.ts.map +1 -0
- package/dist/infra/isInfraShape.js +10 -0
- package/dist/infra/isInfraShape.js.map +1 -0
- package/dist/infra/isInfraWorkloadHealth.d.ts +3 -0
- package/dist/infra/isInfraWorkloadHealth.d.ts.map +1 -0
- package/dist/infra/isInfraWorkloadHealth.js +37 -0
- package/dist/infra/isInfraWorkloadHealth.js.map +1 -0
- package/dist/infra/isInfraWorkloadSpec.d.ts +4 -0
- package/dist/infra/isInfraWorkloadSpec.d.ts.map +1 -0
- package/dist/infra/isInfraWorkloadSpec.js +70 -0
- package/dist/infra/isInfraWorkloadSpec.js.map +1 -0
- package/dist/infra/isInfraWorkloadValue.d.ts +3 -0
- package/dist/infra/isInfraWorkloadValue.d.ts.map +1 -0
- package/dist/infra/isInfraWorkloadValue.js +40 -0
- package/dist/infra/isInfraWorkloadValue.js.map +1 -0
- package/dist/infra/parseInfraManifest.d.ts +5 -0
- package/dist/infra/parseInfraManifest.d.ts.map +1 -0
- package/dist/infra/parseInfraManifest.js +17 -0
- package/dist/infra/parseInfraManifest.js.map +1 -0
- package/dist/infra/validateInfraAdapterSelection.d.ts +6 -0
- package/dist/infra/validateInfraAdapterSelection.d.ts.map +1 -0
- package/dist/infra/validateInfraAdapterSelection.js +36 -0
- package/dist/infra/validateInfraAdapterSelection.js.map +1 -0
- package/dist/infra.d.ts +16 -0
- package/dist/infra.d.ts.map +1 -0
- package/dist/infra.js +11 -0
- package/dist/infra.js.map +1 -0
- package/dist/media.d.ts +7 -6
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +5 -5
- package/dist/media.js.map +1 -1
- package/dist/state.d.ts +5 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/dist/types/appManifest.d.ts +9 -0
- package/dist/types/appManifest.d.ts.map +1 -0
- package/dist/types/appManifest.js +2 -0
- package/dist/types/appManifest.js.map +1 -0
- package/dist/types/infraAdapters.d.ts +85 -0
- package/dist/types/infraAdapters.d.ts.map +1 -0
- package/dist/types/infraAdapters.js +2 -0
- package/dist/types/infraAdapters.js.map +1 -0
- package/dist/types/infraLifecycle.d.ts +107 -0
- package/dist/types/infraLifecycle.d.ts.map +1 -0
- package/dist/types/infraLifecycle.js +2 -0
- package/dist/types/infraLifecycle.js.map +1 -0
- package/dist/types/infraManifest.d.ts +142 -0
- package/dist/types/infraManifest.d.ts.map +1 -0
- package/dist/types/infraManifest.js +2 -0
- package/dist/types/infraManifest.js.map +1 -0
- package/dist/types/infraSecrets.d.ts +15 -0
- package/dist/types/infraSecrets.d.ts.map +1 -0
- package/dist/types/infraSecrets.js +2 -0
- package/dist/types/infraSecrets.js.map +1 -0
- package/dist/types/infraValidation.d.ts +5 -0
- package/dist/types/infraValidation.d.ts.map +1 -0
- package/dist/types/infraValidation.js +2 -0
- package/dist/types/infraValidation.js.map +1 -0
- package/dist/types/infraWorkload.d.ts +70 -0
- package/dist/types/infraWorkload.d.ts.map +1 -0
- package/dist/types/infraWorkload.js +2 -0
- package/dist/types/infraWorkload.js.map +1 -0
- package/dist/types.d.ts +7 -70
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -9
- package/dist/types.js.map +1 -1
- package/package.json +11 -2
- package/src/appManifest/apis.ts +18 -16
- package/src/appManifest/bindings.ts +18 -1
- package/src/appManifest/constants.ts +19 -0
- package/src/appManifest/data.ts +23 -9
- package/src/appManifest/dataSources.ts +8 -7
- package/src/appManifest/deploy.ts +17 -15
- package/src/appManifest/icon.ts +3 -1
- package/src/appManifest/isAppManifest.ts +95 -0
- package/src/appManifest/isAppStateSpec.ts +13 -0
- package/src/appManifest/isManifestValue.ts +19 -0
- package/src/appManifest/media.ts +11 -10
- package/src/appManifest/navigator.ts +7 -6
- package/src/appManifest/navigatorOptions.ts +9 -16
- package/src/appManifest/parseAppManifest.ts +15 -0
- package/src/appManifest/screens.ts +14 -2
- package/src/appManifest.test.ts +9 -5
- package/src/appManifest.ts +3 -107
- package/src/auth-flow-contract.test.ts +6 -10
- package/src/auth-oauth.test.ts +2 -2
- package/src/auth.ts +3 -2
- package/src/bindings.test.ts +5 -0
- package/src/contracts.test.ts +21 -19
- package/src/deploy.test.ts +13 -5
- package/src/deploy.ts +0 -11
- package/src/environments.ts +4 -0
- package/src/index.ts +2 -1
- package/src/infra/constants.ts +91 -0
- package/src/infra/infraFields.ts +20 -0
- package/src/infra/isInfraAdapterDescriptor.ts +37 -0
- package/src/infra/isInfraAuthSpec.ts +98 -0
- package/src/infra/isInfraCredentialRef.ts +11 -0
- package/src/infra/isInfraDeploymentSpec.ts +75 -0
- package/src/infra/isInfraEnvironmentSpec.ts +95 -0
- package/src/infra/isInfraManifest.ts +29 -0
- package/src/infra/isInfraShape.ts +16 -0
- package/src/infra/isInfraWorkloadHealth.ts +38 -0
- package/src/infra/isInfraWorkloadSpec.ts +87 -0
- package/src/infra/isInfraWorkloadValue.ts +41 -0
- package/src/infra/parseInfraManifest.ts +20 -0
- package/src/infra/validateInfraAdapterSelection.ts +42 -0
- package/src/infra-types.test.ts +78 -0
- package/src/infra-workload.test.ts +186 -0
- package/src/infra.test.ts +268 -0
- package/src/infra.ts +15 -0
- package/src/media.ts +12 -11
- package/src/profile-contract.test.ts +2 -3
- package/src/secrets.test.ts +19 -13
- package/src/state.ts +6 -0
- package/src/types/appManifest.ts +5 -0
- package/src/types/infraAdapters.ts +148 -0
- package/src/types/infraLifecycle.ts +105 -0
- package/src/types/infraManifest.ts +130 -0
- package/src/types/infraSecrets.ts +16 -0
- package/src/types/infraValidation.ts +2 -0
- package/src/types/infraWorkload.ts +64 -0
- package/src/types.ts +7 -87
- package/dist/appManifest/infra.d.ts +0 -2
- package/dist/appManifest/infra.d.ts.map +0 -1
- package/dist/appManifest/infra.js +0 -130
- package/dist/appManifest/infra.js.map +0 -1
- package/dist/appManifest/shared.d.ts +0 -8
- package/dist/appManifest/shared.d.ts.map +0 -1
- package/dist/appManifest/shared.js +0 -31
- package/dist/appManifest/shared.js.map +0 -1
- package/dist/secretManifest.d.ts +0 -11
- package/dist/secretManifest.d.ts.map +0 -1
- package/dist/secretManifest.js +0 -2
- package/dist/secretManifest.js.map +0 -1
- package/src/appManifest/infra.ts +0 -190
- package/src/appManifest/shared.ts +0 -40
- package/src/secretManifest.ts +0 -12
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
2
|
+
import { isNonEmptyString } from '@ankhorage/utility/string';
|
|
3
|
+
|
|
4
|
+
import { APP_ENVIRONMENT_IDS } from '../environments';
|
|
5
|
+
import { isInfraShape } from './isInfraShape';
|
|
6
|
+
|
|
7
|
+
/*** Workload values explicitly distinguish public literals, output dependencies and secret references. */
|
|
8
|
+
export function isInfraWorkloadValue(value: unknown): boolean {
|
|
9
|
+
if (!isRecord(value)) return false;
|
|
10
|
+
switch (value.kind) {
|
|
11
|
+
case 'literal':
|
|
12
|
+
return isInfraShape(value, {
|
|
13
|
+
kind: (kind) => kind === 'literal',
|
|
14
|
+
value: (field) => typeof field === 'string',
|
|
15
|
+
});
|
|
16
|
+
case 'output':
|
|
17
|
+
return isInfraShape(value, {
|
|
18
|
+
kind: (kind) => kind === 'output',
|
|
19
|
+
resourceId: isNonEmptyString,
|
|
20
|
+
output: isNonEmptyString,
|
|
21
|
+
});
|
|
22
|
+
case 'secret':
|
|
23
|
+
return isInfraShape(value, {
|
|
24
|
+
kind: (kind) => kind === 'secret',
|
|
25
|
+
reference: isSecretReference,
|
|
26
|
+
});
|
|
27
|
+
default:
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*** Managed secret references contain identity only, never privileged payloads. */
|
|
33
|
+
function isSecretReference(value: unknown): boolean {
|
|
34
|
+
return isInfraShape(value, {
|
|
35
|
+
source: (source) => source === 'secret-store',
|
|
36
|
+
projectId: isNonEmptyString,
|
|
37
|
+
environment: (environment) => APP_ENVIRONMENT_IDS.some((id) => id === environment),
|
|
38
|
+
ref: isNonEmptyString,
|
|
39
|
+
key: isNonEmptyString,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { InfraResult } from '../types/infraLifecycle';
|
|
2
|
+
import type { InfraManifest } from '../types/infraManifest';
|
|
3
|
+
import { isInfraManifest } from './isInfraManifest';
|
|
4
|
+
|
|
5
|
+
/** Parse unknown infrastructure input; package availability and credentials are separate validation stages. */
|
|
6
|
+
export function parseInfraManifest(value: unknown): InfraResult<InfraManifest> {
|
|
7
|
+
return isInfraManifest(value)
|
|
8
|
+
? { ok: true, value, diagnostics: [] }
|
|
9
|
+
: {
|
|
10
|
+
ok: false,
|
|
11
|
+
diagnostics: [
|
|
12
|
+
{
|
|
13
|
+
severity: 'error',
|
|
14
|
+
code: 'invalid_manifest',
|
|
15
|
+
message:
|
|
16
|
+
'Expected canonical Infra environments with supported provider configuration and compute/runtime compatibility.',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { InfraAdapterDescriptor } from '../types/infraAdapters';
|
|
2
|
+
import type { InfraDiagnostic, InfraResult } from '../types/infraLifecycle';
|
|
3
|
+
import type { InfraEnvironmentSpec } from '../types/infraManifest';
|
|
4
|
+
import { INFRA_ADAPTER_CATALOG } from './constants';
|
|
5
|
+
import { isInfraAdapterDescriptor } from './isInfraAdapterDescriptor';
|
|
6
|
+
|
|
7
|
+
/** Validate supplied installed descriptors without imports or filesystem access; only selected packages matter. */
|
|
8
|
+
export function validateInfraAdapterSelection(
|
|
9
|
+
environment: InfraEnvironmentSpec,
|
|
10
|
+
installed: readonly unknown[],
|
|
11
|
+
): InfraResult<readonly InfraAdapterDescriptor[]> {
|
|
12
|
+
const ids = new Set([
|
|
13
|
+
environment.deployment.compute.provider,
|
|
14
|
+
environment.deployment.runtime.provider,
|
|
15
|
+
environment.database?.provider,
|
|
16
|
+
environment.objectStorage?.provider,
|
|
17
|
+
environment.auth?.provider,
|
|
18
|
+
environment.authz?.provider,
|
|
19
|
+
environment.secretStore?.provider,
|
|
20
|
+
]);
|
|
21
|
+
const selected = Object.values(INFRA_ADAPTER_CATALOG).filter((entry) => ids.has(entry.id));
|
|
22
|
+
const descriptors = installed.filter(isInfraAdapterDescriptor);
|
|
23
|
+
const diagnostics: readonly InfraDiagnostic[] = selected.flatMap((entry) => {
|
|
24
|
+
const matches = descriptors.filter((descriptor) => descriptor.id === entry.id);
|
|
25
|
+
return matches.length === 1
|
|
26
|
+
? []
|
|
27
|
+
: [
|
|
28
|
+
{
|
|
29
|
+
severity: 'error',
|
|
30
|
+
code: 'adapter_unavailable',
|
|
31
|
+
message: `Selected adapter ${entry.id} requires exactly one installed ${entry.package} descriptor matching Contracts config version ${entry.configVersion}; found ${matches.length}.`,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
});
|
|
35
|
+
return diagnostics.length > 0
|
|
36
|
+
? { ok: false, diagnostics }
|
|
37
|
+
: {
|
|
38
|
+
ok: true,
|
|
39
|
+
value: descriptors.filter((descriptor) => ids.has(descriptor.id)),
|
|
40
|
+
diagnostics,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import type { AppEnvironmentId } from './environments';
|
|
4
|
+
import type {
|
|
5
|
+
InfraAdapterDescriptor,
|
|
6
|
+
InfraAuthSpec,
|
|
7
|
+
InfraAuthzSpec,
|
|
8
|
+
InfraControlPlaneCredentialRef,
|
|
9
|
+
InfraDeploymentSpec,
|
|
10
|
+
InfraDestroyRequest,
|
|
11
|
+
InfraManifest,
|
|
12
|
+
InfraObjectStorageSpec,
|
|
13
|
+
InfraOutput,
|
|
14
|
+
InfraRuntimeProviderId,
|
|
15
|
+
} from './infra';
|
|
16
|
+
import type { AppStateSpec } from './state';
|
|
17
|
+
|
|
18
|
+
type Assignable<T, U> = [T] extends [U] ? true : false;
|
|
19
|
+
interface Deployment<C, R> {
|
|
20
|
+
readonly compute: C;
|
|
21
|
+
readonly runtime: R;
|
|
22
|
+
}
|
|
23
|
+
interface Local {
|
|
24
|
+
readonly provider: 'local';
|
|
25
|
+
}
|
|
26
|
+
interface Hetzner {
|
|
27
|
+
readonly provider: 'hetzner';
|
|
28
|
+
readonly location: 'nbg1';
|
|
29
|
+
}
|
|
30
|
+
interface Runtime<P extends InfraRuntimeProviderId> {
|
|
31
|
+
readonly provider: P;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it('compiles the entire deployment matrix with exact assignability', () => {
|
|
35
|
+
const matrix: readonly [
|
|
36
|
+
Assignable<Deployment<Local, Runtime<'minikube'>>, InfraDeploymentSpec>,
|
|
37
|
+
Assignable<Deployment<Local, Runtime<'k3s'>>, InfraDeploymentSpec>,
|
|
38
|
+
Assignable<Deployment<Local, Runtime<'docker-compose'>>, InfraDeploymentSpec>,
|
|
39
|
+
Assignable<Deployment<Hetzner, Runtime<'minikube'>>, InfraDeploymentSpec>,
|
|
40
|
+
Assignable<Deployment<Hetzner, Runtime<'k3s'>>, InfraDeploymentSpec>,
|
|
41
|
+
Assignable<Deployment<Hetzner, Runtime<'docker-compose'>>, InfraDeploymentSpec>,
|
|
42
|
+
Assignable<Deployment<{ provider: 'gcp' }, { provider: 'ecs' }>, InfraDeploymentSpec>,
|
|
43
|
+
Assignable<Deployment<Hetzner, { provider: 'eks' }>, InfraDeploymentSpec>,
|
|
44
|
+
Assignable<Deployment<{ provider: 'hetzner' }, Runtime<'k3s'>>, InfraDeploymentSpec>,
|
|
45
|
+
Assignable<Deployment<{ provider: string }, { provider: string }>, InfraDeploymentSpec>,
|
|
46
|
+
] = [true, true, true, false, true, true, false, false, false, false];
|
|
47
|
+
expect(matrix).toEqual([true, true, true, false, true, true, false, false, false, false]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('closes environment, app-state, bootstrap and privileged-output boundaries', () => {
|
|
51
|
+
const closed: readonly [
|
|
52
|
+
Assignable<'staging', AppEnvironmentId>,
|
|
53
|
+
Assignable<{ provider: 'legend'; persistence: 'local' }, AppStateSpec>,
|
|
54
|
+
Assignable<{ provider: 'custom' }, AppStateSpec>,
|
|
55
|
+
Assignable<{ source: 'secret-store'; ref: string }, InfraControlPlaneCredentialRef>,
|
|
56
|
+
Assignable<{ modules: [] }, InfraManifest>,
|
|
57
|
+
Assignable<{ projectId: string; environment: 'production' }, InfraDestroyRequest>,
|
|
58
|
+
Assignable<
|
|
59
|
+
Omit<InfraAdapterDescriptor<'supabase'>, 'package'> & { package: '@ankhorage/supabase-db' },
|
|
60
|
+
InfraAdapterDescriptor
|
|
61
|
+
>,
|
|
62
|
+
Assignable<
|
|
63
|
+
Omit<Extract<InfraOutput, { visibility: 'secret' }>, 'value'> & { value: string },
|
|
64
|
+
InfraOutput
|
|
65
|
+
>,
|
|
66
|
+
] = [false, false, false, false, false, false, false, false];
|
|
67
|
+
expect(closed).toEqual([false, false, false, false, false, false, false, false]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('rejects open or fictional service providers in typed selections', () => {
|
|
71
|
+
const providers: readonly [
|
|
72
|
+
Assignable<{ provider: string }, InfraAuthSpec>,
|
|
73
|
+
Assignable<{ provider: 'native'; kind: 'RBAC' }, InfraAuthzSpec>,
|
|
74
|
+
Assignable<{ provider: 'auto' }, InfraObjectStorageSpec>,
|
|
75
|
+
Assignable<{ provider: 's3' }, InfraObjectStorageSpec>,
|
|
76
|
+
] = [false, false, false, false];
|
|
77
|
+
expect(providers).toEqual([false, false, false, false]);
|
|
78
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { isAppManifest } from './appManifest';
|
|
4
|
+
import {
|
|
5
|
+
type InfraWorkloadSpec,
|
|
6
|
+
isInfraDeploymentSpec,
|
|
7
|
+
isInfraEnvironmentSpec,
|
|
8
|
+
isInfraWorkloadSpec,
|
|
9
|
+
} from './infra';
|
|
10
|
+
import type { AppManifest } from './types';
|
|
11
|
+
|
|
12
|
+
const reference = {
|
|
13
|
+
source: 'secret-store',
|
|
14
|
+
projectId: 'example',
|
|
15
|
+
environment: 'local',
|
|
16
|
+
ref: 'database',
|
|
17
|
+
key: 'password',
|
|
18
|
+
} as const;
|
|
19
|
+
const workload = {
|
|
20
|
+
id: 'backend',
|
|
21
|
+
artifact: { kind: 'image', image: 'example/backend@sha256:abc' },
|
|
22
|
+
command: ['server'],
|
|
23
|
+
args: ['--verbose'],
|
|
24
|
+
ports: [{ name: 'http', port: 8080 }],
|
|
25
|
+
environment: {
|
|
26
|
+
NODE_ENV: { kind: 'literal', value: 'production' },
|
|
27
|
+
DB_PASSWORD: { kind: 'secret', reference },
|
|
28
|
+
DB_HOST: { kind: 'output', resourceId: 'database', output: 'host' },
|
|
29
|
+
},
|
|
30
|
+
files: [{ path: '/etc/backend/config.json', content: { kind: 'literal', value: '{}' } }],
|
|
31
|
+
health: { kind: 'http', port: 8080, path: '/health', intervalSeconds: 5 },
|
|
32
|
+
resources: { cpuMillis: 500, memoryMiB: 256 },
|
|
33
|
+
persistence: [{ id: 'data', mountPath: '/data', sizeGiB: 10, retention: 'retain' }],
|
|
34
|
+
exposure: 'public',
|
|
35
|
+
replicas: 1,
|
|
36
|
+
dependsOn: ['database'],
|
|
37
|
+
} as const satisfies InfraWorkloadSpec;
|
|
38
|
+
|
|
39
|
+
const invalidWorkloads = [
|
|
40
|
+
{ artifact: { kind: 'build', dockerfile: 'Dockerfile' } },
|
|
41
|
+
{ artifact: { kind: 'image', image: '' } },
|
|
42
|
+
{ ports: [{ name: 'http', port: 0 }] },
|
|
43
|
+
{ ports: [{ name: 'http', port: 65536 }] },
|
|
44
|
+
{ ports: [{ name: 'http', port: 1.5 }] },
|
|
45
|
+
{ ports: [{ name: 'http', port: 80, protocol: 'sctp' }] },
|
|
46
|
+
{
|
|
47
|
+
ports: [
|
|
48
|
+
{ name: 'http', port: 80 },
|
|
49
|
+
{ name: 'http', port: 81 },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{ replicas: -1 },
|
|
53
|
+
{ replicas: NaN },
|
|
54
|
+
{ replicas: 1.5 },
|
|
55
|
+
{ resources: { memoryMiB: Infinity } },
|
|
56
|
+
{ resources: { cpuMillis: 0 } },
|
|
57
|
+
{ persistence: [{ id: 'data', mountPath: '/data', sizeGiB: 1 }] },
|
|
58
|
+
{ persistence: [{ id: 'data', mountPath: '../data', sizeGiB: 1, retention: 'retain' }] },
|
|
59
|
+
{ files: [{ path: '/etc/../secret', content: { kind: 'literal', value: 'x' } }] },
|
|
60
|
+
{ health: { kind: 'http', port: 80, path: '/health', failureThreshold: 0 } },
|
|
61
|
+
{ health: { kind: 'tcp', port: 80, path: '/health' } },
|
|
62
|
+
{ health: { kind: 'command', command: [] } },
|
|
63
|
+
{ nodeSelector: {} },
|
|
64
|
+
{ apiVersion: 'apps/v1' },
|
|
65
|
+
{ ingressClassName: 'nginx' },
|
|
66
|
+
{ environment: { PASSWORD: 'plaintext' } },
|
|
67
|
+
{ environment: { PASSWORD: { kind: 'secret', reference, value: 'sentinel-secret' } } },
|
|
68
|
+
{
|
|
69
|
+
environment: { PASSWORD: { kind: 'secret', reference: { ...reference, token: 'sentinel' } } },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
environment: {
|
|
73
|
+
TOKEN: { kind: 'secret', reference: { source: 'control-plane', name: 'HCLOUD_TOKEN' } },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
describe('runtime-neutral workload boundary', () => {
|
|
79
|
+
it('accepts prebuilt images, dependency outputs, config files and secret references on every runtime', () => {
|
|
80
|
+
expect(isInfraWorkloadSpec(JSON.parse(JSON.stringify(workload)))).toBe(true);
|
|
81
|
+
for (const provider of ['minikube', 'k3s', 'docker-compose']) {
|
|
82
|
+
expect(
|
|
83
|
+
isInfraEnvironmentSpec({
|
|
84
|
+
deployment: { compute: { provider: 'local' }, runtime: { provider } },
|
|
85
|
+
workloads: [workload],
|
|
86
|
+
}),
|
|
87
|
+
).toBe(true);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it.each(invalidWorkloads)(
|
|
92
|
+
'rejects invalid or runtime-specific workload fields: %j',
|
|
93
|
+
(override) => {
|
|
94
|
+
expect(isInfraWorkloadSpec({ ...workload, ...override })).toBe(false);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
it('accepts TCP/command health probes and zero replicas for reversible suspension', () => {
|
|
99
|
+
expect(
|
|
100
|
+
isInfraWorkloadSpec({ ...workload, health: { kind: 'tcp', port: 8080 }, replicas: 0 }),
|
|
101
|
+
).toBe(true);
|
|
102
|
+
expect(
|
|
103
|
+
isInfraWorkloadSpec({ ...workload, health: { kind: 'command', command: ['check'] } }),
|
|
104
|
+
).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects duplicate workload identities', () => {
|
|
108
|
+
expect(
|
|
109
|
+
isInfraEnvironmentSpec({
|
|
110
|
+
deployment: { compute: { provider: 'local' }, runtime: { provider: 'k3s' } },
|
|
111
|
+
workloads: [workload, workload],
|
|
112
|
+
}),
|
|
113
|
+
).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('control-plane credential separation', () => {
|
|
118
|
+
it('accepts a bootstrap reference while rejecting managed-store bootstrap and inline tokens', () => {
|
|
119
|
+
for (const credentials of [
|
|
120
|
+
{ source: 'control-plane', name: 'HCLOUD' },
|
|
121
|
+
reference,
|
|
122
|
+
'secret-token',
|
|
123
|
+
{ source: 'control-plane', name: 'HCLOUD', token: 'sentinel' },
|
|
124
|
+
]) {
|
|
125
|
+
const value = {
|
|
126
|
+
compute: { provider: 'hetzner', location: 'nbg1', credentials },
|
|
127
|
+
runtime: { provider: 'k3s' },
|
|
128
|
+
};
|
|
129
|
+
expect(isInfraDeploymentSpec(value)).toBe(
|
|
130
|
+
typeof credentials === 'object' &&
|
|
131
|
+
'source' in credentials &&
|
|
132
|
+
credentials.source === 'control-plane' &&
|
|
133
|
+
!('token' in credentials),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('application state boundary', () => {
|
|
140
|
+
const manifest = {
|
|
141
|
+
metadata: {
|
|
142
|
+
name: 'Example',
|
|
143
|
+
slug: 'example',
|
|
144
|
+
version: '1.0.0',
|
|
145
|
+
category: 'developer_tools',
|
|
146
|
+
themeId: 'default',
|
|
147
|
+
},
|
|
148
|
+
themes: [],
|
|
149
|
+
activeThemeId: 'default',
|
|
150
|
+
screens: {},
|
|
151
|
+
navigator: { type: 'stack', routes: [] },
|
|
152
|
+
infra: {
|
|
153
|
+
environments: {
|
|
154
|
+
local: {
|
|
155
|
+
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
modules: [],
|
|
159
|
+
},
|
|
160
|
+
settings: { localization: { defaultLocale: 'en', locales: ['en'] } },
|
|
161
|
+
} satisfies AppManifest;
|
|
162
|
+
|
|
163
|
+
it('keeps app state optional and accepts only implemented Legend persistence behavior', () => {
|
|
164
|
+
expect(isAppManifest(manifest)).toBe(true);
|
|
165
|
+
expect(isAppManifest({ ...manifest, state: { provider: 'legend' } })).toBe(true);
|
|
166
|
+
expect(isAppManifest({ ...manifest, state: { provider: 'legend', persistence: false } })).toBe(
|
|
167
|
+
true,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it.each(['none', 'local', 'secure', 'database', true, null])(
|
|
172
|
+
'rejects unsupported persistence %j',
|
|
173
|
+
(persistence) => {
|
|
174
|
+
expect(isAppManifest({ ...manifest, state: { provider: 'legend', persistence } })).toBe(
|
|
175
|
+
false,
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
it('rejects open state providers and the obsolete infra.state placement', () => {
|
|
181
|
+
expect(isAppManifest({ ...manifest, state: { provider: 'custom' } })).toBe(false);
|
|
182
|
+
expect(
|
|
183
|
+
isAppManifest({ ...manifest, infra: { ...manifest.infra, state: { provider: 'legend' } } }),
|
|
184
|
+
).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
INFRA_ADAPTER_CATALOG,
|
|
5
|
+
INFRA_RUNTIME_COMPATIBILITY,
|
|
6
|
+
type InfraEnvironmentSpec,
|
|
7
|
+
type InfraManifest,
|
|
8
|
+
isInfraAdapterDescriptor,
|
|
9
|
+
isInfraDeploymentSpec,
|
|
10
|
+
isInfraEnvironmentSpec,
|
|
11
|
+
isInfraManifest,
|
|
12
|
+
parseInfraManifest,
|
|
13
|
+
validateInfraAdapterSelection,
|
|
14
|
+
} from './infra';
|
|
15
|
+
|
|
16
|
+
const local = {
|
|
17
|
+
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
18
|
+
} as const satisfies InfraEnvironmentSpec;
|
|
19
|
+
const production = {
|
|
20
|
+
deployment: {
|
|
21
|
+
compute: { provider: 'hetzner', location: 'nbg1' },
|
|
22
|
+
runtime: { provider: 'k3s', topology: { servers: 1, agents: 0 } },
|
|
23
|
+
},
|
|
24
|
+
database: { provider: 'supabase' },
|
|
25
|
+
auth: { provider: 'supabase' },
|
|
26
|
+
objectStorage: { provider: 'r2', buckets: ['media'] },
|
|
27
|
+
authz: { provider: 'cerbos', kind: 'ABAC' },
|
|
28
|
+
secretStore: { provider: 'supabase-vault' },
|
|
29
|
+
networking: { domain: 'api.example.ch' },
|
|
30
|
+
} as const satisfies InfraEnvironmentSpec;
|
|
31
|
+
|
|
32
|
+
describe('standalone Infra environments', () => {
|
|
33
|
+
it('parses local/preview/production without an AppManifest or Deploy', () => {
|
|
34
|
+
const manifest = {
|
|
35
|
+
environments: { local, preview: production, production },
|
|
36
|
+
modules: [],
|
|
37
|
+
} satisfies InfraManifest;
|
|
38
|
+
expect(parseInfraManifest(JSON.parse(JSON.stringify(manifest)))).toEqual({
|
|
39
|
+
ok: true,
|
|
40
|
+
value: manifest,
|
|
41
|
+
diagnostics: [],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it.each([
|
|
46
|
+
null,
|
|
47
|
+
{},
|
|
48
|
+
{ modules: [] },
|
|
49
|
+
{ environments: {}, modules: [] },
|
|
50
|
+
{ environments: { production }, modules: [] },
|
|
51
|
+
{ environments: { local, staging: local }, modules: [] },
|
|
52
|
+
{ environments: { local, preview: null }, modules: [] },
|
|
53
|
+
{ environments: { local }, modules: [42] },
|
|
54
|
+
{ environments: { local }, modules: [], modulesConfig: [] },
|
|
55
|
+
{ environments: { local }, modules: [], apis: 'invalid' },
|
|
56
|
+
])('rejects invalid environment boundaries: %j', (value) => {
|
|
57
|
+
expect(isInfraManifest(value)).toBe(false);
|
|
58
|
+
expect(parseInfraManifest(value).ok).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each([
|
|
62
|
+
'deployment',
|
|
63
|
+
'storage',
|
|
64
|
+
'state',
|
|
65
|
+
'auth',
|
|
66
|
+
'database',
|
|
67
|
+
'networking',
|
|
68
|
+
'secretStore',
|
|
69
|
+
'plugins',
|
|
70
|
+
'pluginsConfig',
|
|
71
|
+
])('rejects removed top-level %s', (key) => {
|
|
72
|
+
expect(isInfraManifest({ environments: { local }, modules: [], [key]: {} })).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it.each(['storage', 'state', 'monitoring', 'observability', 'target'])(
|
|
76
|
+
'rejects obsolete or speculative environment %s',
|
|
77
|
+
(key) => {
|
|
78
|
+
expect(isInfraEnvironmentSpec({ ...local, [key]: {} })).toBe(false);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('exact deployment compatibility', () => {
|
|
84
|
+
it('tests every registered compute/runtime pair against the same catalog', () => {
|
|
85
|
+
const computes = Object.values(INFRA_ADAPTER_CATALOG).filter(
|
|
86
|
+
(entry) => entry.kind === 'compute',
|
|
87
|
+
);
|
|
88
|
+
for (const compute of computes) {
|
|
89
|
+
for (const [runtime, allowed] of Object.entries(INFRA_RUNTIME_COMPATIBILITY)) {
|
|
90
|
+
const selection = {
|
|
91
|
+
compute: {
|
|
92
|
+
provider: compute.id,
|
|
93
|
+
...(compute.id === 'hetzner' ? { location: 'nbg1' } : {}),
|
|
94
|
+
},
|
|
95
|
+
runtime: { provider: runtime },
|
|
96
|
+
};
|
|
97
|
+
expect(isInfraDeploymentSpec(selection)).toBe(allowed.some((id) => id === compute.id));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it.each([
|
|
103
|
+
{ compute: { provider: 'gcp' }, runtime: { provider: 'ecs' } },
|
|
104
|
+
{ compute: { provider: 'hetzner', location: 'nbg1' }, runtime: { provider: 'eks' } },
|
|
105
|
+
{ compute: { provider: 'hetzner', location: 'nbg1' }, runtime: { provider: 'minikube' } },
|
|
106
|
+
{ compute: { provider: 'hetzner' }, runtime: { provider: 'k3s' } },
|
|
107
|
+
{ compute: { provider: 'hetzner', location: ' ' }, runtime: { provider: 'k3s' } },
|
|
108
|
+
{ compute: { provider: 'local', location: 'nbg1' }, runtime: { provider: 'k3s' } },
|
|
109
|
+
{ ...local.deployment, monitoring: false },
|
|
110
|
+
{ target: 'minikube', monitoring: true },
|
|
111
|
+
])('rejects unsupported/config-mismatched deployment: %j', (value) => {
|
|
112
|
+
expect(isInfraDeploymentSpec(value)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('runtime topology configuration', () => {
|
|
117
|
+
it.each([
|
|
118
|
+
{ provider: 'k3s', topology: { servers: 0, agents: 1 } },
|
|
119
|
+
{ provider: 'k3s', topology: { servers: 1, agents: -1 } },
|
|
120
|
+
{ provider: 'k3s', topology: { servers: 1.5, agents: 0 } },
|
|
121
|
+
{ provider: 'k3s', topology: { servers: 1, agents: Infinity } },
|
|
122
|
+
{ provider: 'k3s', topology: { servers: 1 } },
|
|
123
|
+
{ provider: 'minikube', cpus: 0 },
|
|
124
|
+
{ provider: 'minikube', memoryMiB: NaN },
|
|
125
|
+
{ provider: 'minikube', driver: 'unknown' },
|
|
126
|
+
{ provider: 'docker-compose', profile: 'minikube' },
|
|
127
|
+
])('rejects malformed runtime configuration: %j', (runtime) => {
|
|
128
|
+
expect(isInfraDeploymentSpec({ compute: { provider: 'local' }, runtime })).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('accepts multi-node topology on both supported k3s computes', () => {
|
|
132
|
+
for (const compute of [{ provider: 'local' }, { provider: 'hetzner', location: 'nbg1' }]) {
|
|
133
|
+
expect(
|
|
134
|
+
isInfraDeploymentSpec({
|
|
135
|
+
compute,
|
|
136
|
+
runtime: { provider: 'k3s', topology: { servers: 3, agents: 4 } },
|
|
137
|
+
}),
|
|
138
|
+
).toBe(true);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('sibling service configuration', () => {
|
|
144
|
+
it('accepts Supabase auth, Cerbos and independent R2 storage', () => {
|
|
145
|
+
expect(isInfraEnvironmentSpec(production)).toBe(true);
|
|
146
|
+
expect(isInfraEnvironmentSpec({ ...production, objectStorage: { provider: 'supabase' } })).toBe(
|
|
147
|
+
true,
|
|
148
|
+
);
|
|
149
|
+
expect(isInfraEnvironmentSpec({ ...local, auth: { provider: 'supabase' } })).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it.each([
|
|
153
|
+
{ database: { provider: 'postgres' } },
|
|
154
|
+
{ database: { provider: 'supabase', tier: 'enterprise' } },
|
|
155
|
+
{ objectStorage: { provider: 'auto' } },
|
|
156
|
+
{ objectStorage: { provider: 's3' } },
|
|
157
|
+
{ objectStorage: { provider: 'supabase', buckets: [1] } },
|
|
158
|
+
{ objectStorage: { provider: 'supabase', accountId: 'r2-account' } },
|
|
159
|
+
{ auth: { provider: 'custom' } },
|
|
160
|
+
{ auth: { provider: 'supabase', scope: 'unknown' } },
|
|
161
|
+
{ auth: { provider: 'supabase', authorization: { kind: 'ABAC', engine: 'cerbos' } } },
|
|
162
|
+
{ authz: { provider: 'native', kind: 'RBAC' } },
|
|
163
|
+
{ authz: { engine: 'cerbos', kind: 'ABAC' } },
|
|
164
|
+
{ authz: { provider: 'cerbos', kind: 'unknown' } },
|
|
165
|
+
{ secretStore: { provider: 'unknown' } },
|
|
166
|
+
{ secretStore: { provider: 'supabase-vault' } },
|
|
167
|
+
{ networking: { domain: 'example.ch', cdn: true } },
|
|
168
|
+
])('rejects unsupported service selection: %j', (selection) => {
|
|
169
|
+
expect(isInfraEnvironmentSpec({ ...local, ...selection })).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('authentication configuration', () => {
|
|
174
|
+
it('preserves auth flow, profile and OAuth credential references', () => {
|
|
175
|
+
expect(
|
|
176
|
+
isInfraEnvironmentSpec({
|
|
177
|
+
...production,
|
|
178
|
+
auth: {
|
|
179
|
+
provider: 'supabase',
|
|
180
|
+
scope: 'global',
|
|
181
|
+
flow: { signInRoute: '/sign-in', postSignInRoute: '/' },
|
|
182
|
+
signIn: { identifiers: ['email'] },
|
|
183
|
+
signUp: { requiredFields: ['email', 'password'], signUpPolicy: 'requireVerification' },
|
|
184
|
+
profile: {
|
|
185
|
+
fields: ['email'],
|
|
186
|
+
primaryKey: 'authUserId',
|
|
187
|
+
createStrategy: 'trigger',
|
|
188
|
+
updateStrategy: 'api',
|
|
189
|
+
},
|
|
190
|
+
oauth: {
|
|
191
|
+
enabled: true,
|
|
192
|
+
callbackRoute: '/auth/callback',
|
|
193
|
+
providers: [{ id: 'google', credentialsRef: 'auth/oauth/google' }],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it.each([
|
|
201
|
+
{ flow: { signInRoute: '/' } },
|
|
202
|
+
{ signIn: { identifiers: ['social'] } },
|
|
203
|
+
{ signUp: { requiredFields: [], signUpPolicy: 'unknown' } },
|
|
204
|
+
{ profile: { fields: ['email'], createStrategy: 'unknown' } },
|
|
205
|
+
{
|
|
206
|
+
oauth: {
|
|
207
|
+
enabled: true,
|
|
208
|
+
callbackRoute: '/',
|
|
209
|
+
providers: [{ id: 'google', clientSecret: 'sentinel' }],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
])('rejects malformed auth configuration: %j', (config) => {
|
|
213
|
+
expect(isInfraEnvironmentSpec({ ...local, auth: { provider: 'supabase', ...config } })).toBe(
|
|
214
|
+
false,
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('adapter package discovery contracts', () => {
|
|
220
|
+
it('accepts every canonical descriptor and deduplicates a multi-capability Supabase platform', () => {
|
|
221
|
+
for (const descriptor of Object.values(INFRA_ADAPTER_CATALOG))
|
|
222
|
+
expect(isInfraAdapterDescriptor(descriptor)).toBe(true);
|
|
223
|
+
const environment = { ...production, objectStorage: { provider: 'supabase' } } as const;
|
|
224
|
+
const result = validateInfraAdapterSelection(environment, Object.values(INFRA_ADAPTER_CATALOG));
|
|
225
|
+
expect(result.ok).toBe(true);
|
|
226
|
+
if (!result.ok) throw new Error('Expected installed descriptors.');
|
|
227
|
+
expect(result.value.filter((descriptor) => descriptor.id === 'supabase')).toHaveLength(1);
|
|
228
|
+
expect(result.value.some((descriptor) => descriptor.id === 'r2')).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it.each([
|
|
232
|
+
{ package: '@ankhorage/supabase-db' },
|
|
233
|
+
{ id: 'custom' },
|
|
234
|
+
{ kind: 'runtime' },
|
|
235
|
+
{ configVersion: 2 },
|
|
236
|
+
{ capabilities: ['auth'] },
|
|
237
|
+
{ dependencies: [] },
|
|
238
|
+
{ targets: ['local-host'] },
|
|
239
|
+
{ operations: ['drop-all'] },
|
|
240
|
+
])('rejects a mismatched package descriptor: %j', (override) => {
|
|
241
|
+
expect(isInfraAdapterDescriptor({ ...INFRA_ADAPTER_CATALOG.supabase, ...override })).toBe(
|
|
242
|
+
false,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('reports the exact missing selected package without requiring unselected providers', () => {
|
|
247
|
+
expect(
|
|
248
|
+
validateInfraAdapterSelection(local, [
|
|
249
|
+
INFRA_ADAPTER_CATALOG.local,
|
|
250
|
+
INFRA_ADAPTER_CATALOG.minikube,
|
|
251
|
+
]).ok,
|
|
252
|
+
).toBe(true);
|
|
253
|
+
const result = validateInfraAdapterSelection(local, [INFRA_ADAPTER_CATALOG.local]);
|
|
254
|
+
expect(result.ok).toBe(false);
|
|
255
|
+
expect(result.diagnostics[0]?.message).toContain('@ankhorage/minikube');
|
|
256
|
+
expect(result.diagnostics[0]?.code).toBe('adapter_unavailable');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('rejects ambiguous duplicate installed descriptors', () => {
|
|
260
|
+
expect(
|
|
261
|
+
validateInfraAdapterSelection(local, [
|
|
262
|
+
INFRA_ADAPTER_CATALOG.local,
|
|
263
|
+
INFRA_ADAPTER_CATALOG.local,
|
|
264
|
+
INFRA_ADAPTER_CATALOG.minikube,
|
|
265
|
+
]).ok,
|
|
266
|
+
).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
});
|
package/src/infra.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Canonical standalone infrastructure contracts, catalog and side-effect-free validation. */
|
|
2
|
+
export { INFRA_ADAPTER_CATALOG, INFRA_RUNTIME_COMPATIBILITY } from './infra/constants';
|
|
3
|
+
export { isInfraAdapterDescriptor } from './infra/isInfraAdapterDescriptor';
|
|
4
|
+
export { isInfraAuthSpec } from './infra/isInfraAuthSpec';
|
|
5
|
+
export { isInfraDeploymentSpec } from './infra/isInfraDeploymentSpec';
|
|
6
|
+
export { isInfraEnvironmentSpec } from './infra/isInfraEnvironmentSpec';
|
|
7
|
+
export { isInfraManifest } from './infra/isInfraManifest';
|
|
8
|
+
export { isInfraWorkloadSpec } from './infra/isInfraWorkloadSpec';
|
|
9
|
+
export { parseInfraManifest } from './infra/parseInfraManifest';
|
|
10
|
+
export { validateInfraAdapterSelection } from './infra/validateInfraAdapterSelection';
|
|
11
|
+
export type * from './types/infraAdapters';
|
|
12
|
+
export type * from './types/infraLifecycle';
|
|
13
|
+
export type * from './types/infraManifest';
|
|
14
|
+
export type * from './types/infraSecrets';
|
|
15
|
+
export type * from './types/infraWorkload';
|