@ankhorage/contracts 13.0.1 → 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 +37 -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/splashScreen.d.ts +15 -0
- package/dist/appManifest/splashScreen.d.ts.map +1 -0
- package/dist/appManifest/splashScreen.js +2 -0
- package/dist/appManifest/splashScreen.js.map +1 -0
- package/dist/appManifest.d.ts +4 -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 +8 -82
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -9
- package/dist/types.js.map +1 -1
- package/package.json +13 -3
- 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/splashScreen.test.ts +40 -0
- package/src/appManifest/splashScreen.ts +24 -0
- package/src/appManifest.test.ts +9 -5
- package/src/appManifest.ts +8 -106
- 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 -39
- 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 +8 -103
- 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,29 @@
|
|
|
1
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
2
|
+
|
|
3
|
+
import { isApiDefinitionList } from '../appManifest/apis';
|
|
4
|
+
import { APP_ENVIRONMENT_IDS } from '../environments';
|
|
5
|
+
import type { InfraManifest } from '../types/infraManifest';
|
|
6
|
+
import type { InfraShape } from '../types/infraValidation';
|
|
7
|
+
import { infraFields } from './infraFields';
|
|
8
|
+
import { isInfraEnvironmentSpec } from './isInfraEnvironmentSpec';
|
|
9
|
+
import { isInfraShape } from './isInfraShape';
|
|
10
|
+
|
|
11
|
+
/*** Validate standalone infrastructure without AppManifest, Deploy or provider package side effects. */
|
|
12
|
+
export function isInfraManifest(value: unknown): value is InfraManifest {
|
|
13
|
+
return isInfraShape(value, {
|
|
14
|
+
environments: isEnvironments,
|
|
15
|
+
apis: (apis) => apis === undefined || isApiDefinitionList(apis),
|
|
16
|
+
modules: infraFields.strings,
|
|
17
|
+
modulesConfig: (config) => config === undefined || isRecord(config),
|
|
18
|
+
} satisfies InfraShape<InfraManifest>);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/*** Local is required; preview and production are optional but never inferred from arbitrary keys. */
|
|
22
|
+
function isEnvironments(value: unknown): boolean {
|
|
23
|
+
if (!isRecord(value) || !isInfraEnvironmentSpec(value.local)) return false;
|
|
24
|
+
return Object.entries(value).every(
|
|
25
|
+
([key, environment]) =>
|
|
26
|
+
APP_ENVIRONMENT_IDS.some((id) => id === key) &&
|
|
27
|
+
((environment === undefined && key !== 'local') || isInfraEnvironmentSpec(environment)),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { hasOnlyKeys, isRecord } from '@ankhorage/utility/object';
|
|
2
|
+
|
|
3
|
+
/*** Validate an exact Infra object shape, rejecting unknown and superseded configuration keys. */
|
|
4
|
+
export function isInfraShape(
|
|
5
|
+
value: unknown,
|
|
6
|
+
fields: Readonly<Record<string, (field: unknown) => boolean>>,
|
|
7
|
+
): boolean {
|
|
8
|
+
if (!isRecord(value)) return false;
|
|
9
|
+
const entries = Object.entries(fields);
|
|
10
|
+
return (
|
|
11
|
+
hasOnlyKeys(value, Object.keys(fields)) &&
|
|
12
|
+
entries.every(([key, validate]) =>
|
|
13
|
+
validate(Object.hasOwn(value, key) ? Reflect.get(value, key) : undefined),
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
2
|
+
|
|
3
|
+
import { infraFields } from './infraFields';
|
|
4
|
+
import { isInfraShape } from './isInfraShape';
|
|
5
|
+
|
|
6
|
+
/*** Validate runtime-neutral readiness probes and bounded numeric parameters. */
|
|
7
|
+
export function isInfraWorkloadHealth(value: unknown): boolean {
|
|
8
|
+
if (!isRecord(value)) return false;
|
|
9
|
+
const timing = {
|
|
10
|
+
intervalSeconds: infraFields.optionalPositiveInteger,
|
|
11
|
+
timeoutSeconds: infraFields.optionalPositiveInteger,
|
|
12
|
+
failureThreshold: infraFields.optionalPositiveInteger,
|
|
13
|
+
};
|
|
14
|
+
switch (value.kind) {
|
|
15
|
+
case 'http':
|
|
16
|
+
return isInfraShape(value, {
|
|
17
|
+
...timing,
|
|
18
|
+
kind: (kind) => kind === 'http',
|
|
19
|
+
port: infraFields.port,
|
|
20
|
+
path: (path) => typeof path === 'string' && path.startsWith('/'),
|
|
21
|
+
});
|
|
22
|
+
case 'tcp':
|
|
23
|
+
return isInfraShape(value, {
|
|
24
|
+
...timing,
|
|
25
|
+
kind: (kind) => kind === 'tcp',
|
|
26
|
+
port: infraFields.port,
|
|
27
|
+
});
|
|
28
|
+
case 'command':
|
|
29
|
+
return isInfraShape(value, {
|
|
30
|
+
...timing,
|
|
31
|
+
kind: (kind) => kind === 'command',
|
|
32
|
+
command: (command) =>
|
|
33
|
+
Array.isArray(command) && command.length > 0 && infraFields.strings(command),
|
|
34
|
+
});
|
|
35
|
+
default:
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isStringArray } from '@ankhorage/utility/array';
|
|
2
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
3
|
+
import { isNonEmptyString } from '@ankhorage/utility/string';
|
|
4
|
+
|
|
5
|
+
import type { InfraShape } from '../types/infraValidation';
|
|
6
|
+
import type { InfraWorkloadSpec } from '../types/infraWorkload';
|
|
7
|
+
import { infraFields } from './infraFields';
|
|
8
|
+
import { isInfraShape } from './isInfraShape';
|
|
9
|
+
import { isInfraWorkloadHealth } from './isInfraWorkloadHealth';
|
|
10
|
+
import { isInfraWorkloadValue } from './isInfraWorkloadValue';
|
|
11
|
+
|
|
12
|
+
/*** Validate the portable desired workload; runtime-specific fields and plaintext secret objects fail. */
|
|
13
|
+
export function isInfraWorkloadSpec(value: unknown): value is InfraWorkloadSpec {
|
|
14
|
+
return isInfraShape(value, {
|
|
15
|
+
id: isNonEmptyString,
|
|
16
|
+
artifact: (artifact) =>
|
|
17
|
+
isInfraShape(artifact, { kind: (kind) => kind === 'image', image: isNonEmptyString }),
|
|
18
|
+
command: infraFields.optionalStrings,
|
|
19
|
+
args: (args) => args === undefined || isStringArray(args),
|
|
20
|
+
ports: (ports) =>
|
|
21
|
+
ports === undefined ||
|
|
22
|
+
(Array.isArray(ports) && ports.every(isPort) && hasUniqueField(ports, 'name')),
|
|
23
|
+
environment: (environment) =>
|
|
24
|
+
environment === undefined ||
|
|
25
|
+
(isRecord(environment) && Object.values(environment).every(isInfraWorkloadValue)),
|
|
26
|
+
files: (files) =>
|
|
27
|
+
files === undefined ||
|
|
28
|
+
(Array.isArray(files) && files.every(isFile) && hasUniqueField(files, 'path')),
|
|
29
|
+
health: (health) => health === undefined || isInfraWorkloadHealth(health),
|
|
30
|
+
resources: (resources) =>
|
|
31
|
+
resources === undefined ||
|
|
32
|
+
isInfraShape(resources, {
|
|
33
|
+
cpuMillis: infraFields.optionalPositiveInteger,
|
|
34
|
+
memoryMiB: infraFields.optionalPositiveInteger,
|
|
35
|
+
}),
|
|
36
|
+
persistence: (volumes) =>
|
|
37
|
+
volumes === undefined ||
|
|
38
|
+
(Array.isArray(volumes) &&
|
|
39
|
+
volumes.every(isVolume) &&
|
|
40
|
+
hasUniqueField(volumes, 'id') &&
|
|
41
|
+
hasUniqueField(volumes, 'mountPath')),
|
|
42
|
+
exposure: (exposure) =>
|
|
43
|
+
exposure === undefined || exposure === 'internal' || exposure === 'public',
|
|
44
|
+
replicas: infraFields.optionalNonnegativeInteger,
|
|
45
|
+
dependsOn: infraFields.optionalStrings,
|
|
46
|
+
} satisfies InfraShape<InfraWorkloadSpec>);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*** Named transport ports stay independent from runtime resource types. */
|
|
50
|
+
function isPort(value: unknown): boolean {
|
|
51
|
+
return isInfraShape(value, {
|
|
52
|
+
name: isNonEmptyString,
|
|
53
|
+
port: infraFields.port,
|
|
54
|
+
protocol: (protocol) => protocol === undefined || protocol === 'tcp' || protocol === 'udp',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*** Portable files model policy/config materialization without host filesystem access. */
|
|
59
|
+
function isFile(value: unknown): boolean {
|
|
60
|
+
return isInfraShape(value, { path: isAbsoluteWorkloadPath, content: isInfraWorkloadValue });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/*** Persistence has explicit ownership-local identity and a retention policy. */
|
|
64
|
+
function isVolume(value: unknown): boolean {
|
|
65
|
+
return isInfraShape(value, {
|
|
66
|
+
id: isNonEmptyString,
|
|
67
|
+
mountPath: isAbsoluteWorkloadPath,
|
|
68
|
+
sizeGiB: infraFields.positiveInteger,
|
|
69
|
+
retention: (retention) => retention === 'retain' || retention === 'delete-on-destroy',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*** Workload paths are absolute container paths, never relative host traversal. */
|
|
74
|
+
function isAbsoluteWorkloadPath(value: unknown): boolean {
|
|
75
|
+
return (
|
|
76
|
+
typeof value === 'string' &&
|
|
77
|
+
value.startsWith('/') &&
|
|
78
|
+
value.length > 1 &&
|
|
79
|
+
!value.split('/').includes('..')
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*** Duplicate workload-local identities would make runtime projection ambiguous. */
|
|
84
|
+
function hasUniqueField(values: readonly unknown[], field: string): boolean {
|
|
85
|
+
const ids = values.map((value) => (isRecord(value) ? Reflect.get(value, field) : undefined));
|
|
86
|
+
return new Set(ids).size === ids.length;
|
|
87
|
+
}
|
|
@@ -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
|
+
});
|