@ankhorage/contracts 14.0.0 → 16.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.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 +87 -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 +93 -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 +150 -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
package/src/deploy.ts
CHANGED
|
@@ -2,17 +2,6 @@ export const APP_DEPLOY_TARGET_IDS = ['web', 'android', 'ios'] as const;
|
|
|
2
2
|
|
|
3
3
|
export type AppDeployTargetId = (typeof APP_DEPLOY_TARGET_IDS)[number];
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Logical application environments shared across platform-specific planning.
|
|
7
|
-
*
|
|
8
|
-
* `local` maps to local Web, iOS development, and Android debug execution.
|
|
9
|
-
* Environment-specific endpoints, signing material, and credentials are owned
|
|
10
|
-
* by their dedicated deployment/infra configuration rather than this identity contract.
|
|
11
|
-
*/
|
|
12
|
-
export const APP_DEPLOY_ENVIRONMENT_IDS = ['local', 'preview', 'production'] as const;
|
|
13
|
-
|
|
14
|
-
export type AppDeployEnvironmentId = (typeof APP_DEPLOY_ENVIRONMENT_IDS)[number];
|
|
15
|
-
|
|
16
5
|
export interface AppDeployProviderSelection {
|
|
17
6
|
readonly build?: string;
|
|
18
7
|
readonly publish?: string;
|
package/src/index.ts
CHANGED
|
@@ -5,12 +5,13 @@ export * from './cli';
|
|
|
5
5
|
export * from './data';
|
|
6
6
|
export * from './db';
|
|
7
7
|
export * from './deploy';
|
|
8
|
+
export * from './environments';
|
|
9
|
+
export * from './infra';
|
|
8
10
|
export * from './media';
|
|
9
11
|
export * from './navigator';
|
|
10
12
|
export * from './repository';
|
|
11
13
|
export * from './requirements';
|
|
12
14
|
export * from './runtimeCallbacks';
|
|
13
|
-
export * from './secretManifest';
|
|
14
15
|
export * from './secrets';
|
|
15
16
|
export * from './state';
|
|
16
17
|
export * from './storage';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** One compatibility source for manifest types, validation, discovery and configuration UIs. */
|
|
2
|
+
export const INFRA_RUNTIME_COMPATIBILITY = {
|
|
3
|
+
minikube: ['local'],
|
|
4
|
+
k3s: ['local', 'hetzner'],
|
|
5
|
+
'docker-compose': ['local', 'hetzner'],
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
/** Canonical package identities. Capabilities describe ownership, not eight mandatory methods. */
|
|
9
|
+
export const INFRA_ADAPTER_CATALOG = {
|
|
10
|
+
local: {
|
|
11
|
+
id: 'local',
|
|
12
|
+
package: '@ankhorage/local',
|
|
13
|
+
kind: 'compute',
|
|
14
|
+
configVersion: 1,
|
|
15
|
+
capabilities: ['compute'],
|
|
16
|
+
targets: ['local-host'],
|
|
17
|
+
dependencies: [],
|
|
18
|
+
},
|
|
19
|
+
hetzner: {
|
|
20
|
+
id: 'hetzner',
|
|
21
|
+
package: '@ankhorage/hetzner',
|
|
22
|
+
kind: 'compute',
|
|
23
|
+
configVersion: 1,
|
|
24
|
+
capabilities: ['compute'],
|
|
25
|
+
targets: ['ssh-host'],
|
|
26
|
+
dependencies: [],
|
|
27
|
+
},
|
|
28
|
+
minikube: {
|
|
29
|
+
id: 'minikube',
|
|
30
|
+
package: '@ankhorage/minikube',
|
|
31
|
+
kind: 'runtime',
|
|
32
|
+
configVersion: 1,
|
|
33
|
+
capabilities: ['runtime', 'workloads', 'networking'],
|
|
34
|
+
targets: ['local-host'],
|
|
35
|
+
dependencies: ['compute'],
|
|
36
|
+
},
|
|
37
|
+
k3s: {
|
|
38
|
+
id: 'k3s',
|
|
39
|
+
package: '@ankhorage/k3s',
|
|
40
|
+
kind: 'runtime',
|
|
41
|
+
configVersion: 1,
|
|
42
|
+
capabilities: ['runtime', 'workloads', 'networking'],
|
|
43
|
+
targets: ['local-host', 'ssh-host'],
|
|
44
|
+
dependencies: ['compute'],
|
|
45
|
+
},
|
|
46
|
+
'docker-compose': {
|
|
47
|
+
id: 'docker-compose',
|
|
48
|
+
package: '@ankhorage/docker-compose',
|
|
49
|
+
kind: 'runtime',
|
|
50
|
+
configVersion: 1,
|
|
51
|
+
capabilities: ['runtime', 'workloads', 'networking'],
|
|
52
|
+
targets: ['local-host', 'ssh-host'],
|
|
53
|
+
dependencies: ['compute'],
|
|
54
|
+
},
|
|
55
|
+
supabase: {
|
|
56
|
+
id: 'supabase',
|
|
57
|
+
package: '@ankhorage/supabase',
|
|
58
|
+
kind: 'service',
|
|
59
|
+
configVersion: 1,
|
|
60
|
+
capabilities: ['database', 'auth', 'objectStorage'],
|
|
61
|
+
targets: [],
|
|
62
|
+
dependencies: ['workloads'],
|
|
63
|
+
},
|
|
64
|
+
cerbos: {
|
|
65
|
+
id: 'cerbos',
|
|
66
|
+
package: '@ankhorage/cerbos',
|
|
67
|
+
kind: 'service',
|
|
68
|
+
configVersion: 1,
|
|
69
|
+
capabilities: ['authz'],
|
|
70
|
+
targets: [],
|
|
71
|
+
dependencies: ['workloads'],
|
|
72
|
+
},
|
|
73
|
+
r2: {
|
|
74
|
+
id: 'r2',
|
|
75
|
+
package: '@ankhorage/r2',
|
|
76
|
+
kind: 'service',
|
|
77
|
+
configVersion: 1,
|
|
78
|
+
capabilities: ['objectStorage'],
|
|
79
|
+
targets: [],
|
|
80
|
+
dependencies: [],
|
|
81
|
+
},
|
|
82
|
+
'supabase-vault': {
|
|
83
|
+
id: 'supabase-vault',
|
|
84
|
+
package: '@ankhorage/supabase-vault',
|
|
85
|
+
kind: 'service',
|
|
86
|
+
configVersion: 1,
|
|
87
|
+
capabilities: ['secretStore'],
|
|
88
|
+
targets: [],
|
|
89
|
+
dependencies: ['database'],
|
|
90
|
+
},
|
|
91
|
+
} as const;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isStringArray } from '@ankhorage/utility/array';
|
|
2
|
+
import { isNonEmptyString } from '@ankhorage/utility/string';
|
|
3
|
+
|
|
4
|
+
/*** Primitive field rules for portable Infra configuration, shared by its exact-shape validators. */
|
|
5
|
+
export const infraFields = {
|
|
6
|
+
optionalText: (value: unknown): boolean => value === undefined || isNonEmptyString(value),
|
|
7
|
+
strings: (value: unknown): boolean => isStringArray(value) && value.every(isNonEmptyString),
|
|
8
|
+
optionalStrings: (value: unknown): boolean =>
|
|
9
|
+
value === undefined || (isStringArray(value) && value.every(isNonEmptyString)),
|
|
10
|
+
positiveInteger: (value: unknown): boolean =>
|
|
11
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value > 0,
|
|
12
|
+
nonnegativeInteger: (value: unknown): boolean =>
|
|
13
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value >= 0,
|
|
14
|
+
optionalPositiveInteger: (value: unknown): boolean =>
|
|
15
|
+
value === undefined || (typeof value === 'number' && Number.isSafeInteger(value) && value > 0),
|
|
16
|
+
optionalNonnegativeInteger: (value: unknown): boolean =>
|
|
17
|
+
value === undefined || (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0),
|
|
18
|
+
port: (value: unknown): boolean =>
|
|
19
|
+
typeof value === 'number' && Number.isInteger(value) && value > 0 && value <= 65535,
|
|
20
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
2
|
+
|
|
3
|
+
import type { InfraAdapterDescriptor } from '../types/infraAdapters';
|
|
4
|
+
import { INFRA_ADAPTER_CATALOG } from './constants';
|
|
5
|
+
import { isInfraShape } from './isInfraShape';
|
|
6
|
+
|
|
7
|
+
/*** Installed adapters must match the canonical identity, capabilities, targets and config version exactly. */
|
|
8
|
+
export function isInfraAdapterDescriptor(value: unknown): value is InfraAdapterDescriptor {
|
|
9
|
+
if (!isRecord(value)) return false;
|
|
10
|
+
const entry = Object.values(INFRA_ADAPTER_CATALOG).find((candidate) => candidate.id === value.id);
|
|
11
|
+
if (!entry) return false;
|
|
12
|
+
return isInfraShape(value, {
|
|
13
|
+
id: (id) => id === entry.id,
|
|
14
|
+
package: (name) => name === entry.package,
|
|
15
|
+
kind: (kind) => kind === entry.kind,
|
|
16
|
+
configVersion: (version) => version === entry.configVersion,
|
|
17
|
+
capabilities: (items) => matchesCatalogList(items, entry.capabilities),
|
|
18
|
+
targets: (items) => matchesCatalogList(items, entry.targets),
|
|
19
|
+
dependencies: (items) => matchesCatalogList(items, entry.dependencies),
|
|
20
|
+
operations: (items) =>
|
|
21
|
+
items === undefined ||
|
|
22
|
+
(Array.isArray(items) &&
|
|
23
|
+
new Set(items).size === items.length &&
|
|
24
|
+
items.every((item: unknown) =>
|
|
25
|
+
['suspend', 'resume', 'generate', 'diagnostics'].some((operation) => operation === item),
|
|
26
|
+
)),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*** Canonical descriptor lists are ordered so JSON discovery and conformance are deterministic. */
|
|
31
|
+
function matchesCatalogList(value: unknown, expected: readonly string[]): boolean {
|
|
32
|
+
return (
|
|
33
|
+
Array.isArray(value) &&
|
|
34
|
+
value.length === expected.length &&
|
|
35
|
+
value.every((item, index) => item === expected.at(index))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { isStringArray } from '@ankhorage/utility/array';
|
|
2
|
+
import { isRecordOf } from '@ankhorage/utility/object';
|
|
3
|
+
import { isOptionalString } from '@ankhorage/utility/string';
|
|
4
|
+
|
|
5
|
+
import { isIconSpec } from '../appManifest/icon';
|
|
6
|
+
import { AUTH_OAUTH_PROVIDER_IDS } from '../auth';
|
|
7
|
+
import {
|
|
8
|
+
AUTH_PROFILE_CREATE_STRATEGIES,
|
|
9
|
+
AUTH_PROFILE_PRIMARY_KEY_STRATEGIES,
|
|
10
|
+
AUTH_PROFILE_UPDATE_STRATEGIES,
|
|
11
|
+
AUTH_SCOPES,
|
|
12
|
+
AUTH_SIGN_IN_IDENTIFIERS,
|
|
13
|
+
AUTH_SIGN_UP_POLICIES,
|
|
14
|
+
} from '../types';
|
|
15
|
+
import type { InfraAuthSpec } from '../types/infraManifest';
|
|
16
|
+
import type { InfraShape } from '../types/infraValidation';
|
|
17
|
+
import { isInfraShape } from './isInfraShape';
|
|
18
|
+
|
|
19
|
+
/*** Preserve application auth configuration while rejecting nested authorization and unknown providers. */
|
|
20
|
+
export function isInfraAuthSpec(value: unknown): value is InfraAuthSpec {
|
|
21
|
+
return isInfraShape(value, {
|
|
22
|
+
provider: (provider) => provider === 'supabase',
|
|
23
|
+
scope: (scope) => scope === undefined || AUTH_SCOPES.some((item) => item === scope),
|
|
24
|
+
flow: (flow) => flow === undefined || isAuthFlow(flow),
|
|
25
|
+
signIn: (signIn) =>
|
|
26
|
+
signIn === undefined ||
|
|
27
|
+
isInfraShape(signIn, {
|
|
28
|
+
identifiers: (ids) =>
|
|
29
|
+
Array.isArray(ids) &&
|
|
30
|
+
ids.every((id) => AUTH_SIGN_IN_IDENTIFIERS.some((item) => item === id)),
|
|
31
|
+
}),
|
|
32
|
+
signUp: (signUp) => signUp === undefined || isAuthSignUp(signUp),
|
|
33
|
+
oauth: (oauth) => oauth === undefined || isAuthOAuth(oauth),
|
|
34
|
+
profile: (profile) => profile === undefined || isAuthProfile(profile),
|
|
35
|
+
} satisfies InfraShape<InfraAuthSpec>);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/*** Auth routing intent stays separate from provider-runtime callbacks and Navigator topology. */
|
|
39
|
+
function isAuthFlow(value: unknown): boolean {
|
|
40
|
+
return isInfraShape(value, {
|
|
41
|
+
signInRoute: (route) => typeof route === 'string',
|
|
42
|
+
postSignInRoute: (route) => typeof route === 'string',
|
|
43
|
+
signUpRoute: isOptionalString,
|
|
44
|
+
signOutRoute: isOptionalString,
|
|
45
|
+
forgotPasswordRoute: isOptionalString,
|
|
46
|
+
otpRoute: isOptionalString,
|
|
47
|
+
unauthorizedRoute: isOptionalString,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/*** Validate sign-up field intent using the existing application auth contract. */
|
|
52
|
+
function isAuthSignUp(value: unknown): boolean {
|
|
53
|
+
return isInfraShape(value, {
|
|
54
|
+
requiredFields: isStringArray,
|
|
55
|
+
optionalFields: (fields) => fields === undefined || isStringArray(fields),
|
|
56
|
+
signUpPolicy: (policy) =>
|
|
57
|
+
policy === undefined || AUTH_SIGN_UP_POLICIES.some((item) => item === policy),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*** OAuth references contain no credential payload. Custom OAuth identities remain application-owned. */
|
|
62
|
+
function isAuthOAuth(value: unknown): boolean {
|
|
63
|
+
return isInfraShape(value, {
|
|
64
|
+
enabled: (enabled) => typeof enabled === 'boolean',
|
|
65
|
+
callbackRoute: (route) => typeof route === 'string',
|
|
66
|
+
providers: (providers) => Array.isArray(providers) && providers.every(isAuthOAuthProvider),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/*** Validate OAuth display/config metadata, preserving explicitly extensible OAuth provider IDs. */
|
|
71
|
+
function isAuthOAuthProvider(value: unknown): boolean {
|
|
72
|
+
return isInfraShape(value, {
|
|
73
|
+
id: (id) =>
|
|
74
|
+
typeof id === 'string' &&
|
|
75
|
+
(AUTH_OAUTH_PROVIDER_IDS.some((item) => item === id) || id.length > 0),
|
|
76
|
+
label: isOptionalString,
|
|
77
|
+
enabled: (value) => value === undefined || typeof value === 'boolean',
|
|
78
|
+
scopes: (scopes) => scopes === undefined || isStringArray(scopes),
|
|
79
|
+
queryParams: (params) =>
|
|
80
|
+
params === undefined || isRecordOf(params, (entry) => typeof entry === 'string'),
|
|
81
|
+
icon: (icon) => icon === undefined || isIconSpec(icon),
|
|
82
|
+
credentialsRef: isOptionalString,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/*** Validate profile provisioning choices without inventing a new application profile model. */
|
|
87
|
+
function isAuthProfile(value: unknown): boolean {
|
|
88
|
+
return isInfraShape(value, {
|
|
89
|
+
fields: isStringArray,
|
|
90
|
+
table: isOptionalString,
|
|
91
|
+
primaryKey: (key) =>
|
|
92
|
+
key === undefined || AUTH_PROFILE_PRIMARY_KEY_STRATEGIES.some((item) => item === key),
|
|
93
|
+
createStrategy: (strategy) =>
|
|
94
|
+
strategy === undefined || AUTH_PROFILE_CREATE_STRATEGIES.some((item) => item === strategy),
|
|
95
|
+
updateStrategy: (strategy) =>
|
|
96
|
+
strategy === undefined || AUTH_PROFILE_UPDATE_STRATEGIES.some((item) => item === strategy),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isNonEmptyString } from '@ankhorage/utility/string';
|
|
2
|
+
|
|
3
|
+
import { isInfraShape } from './isInfraShape';
|
|
4
|
+
|
|
5
|
+
/*** Bootstrap credential references are explicitly separate from managed runtime secrets. */
|
|
6
|
+
export function isInfraCredentialRef(value: unknown): boolean {
|
|
7
|
+
return isInfraShape(value, {
|
|
8
|
+
source: (source) => source === 'control-plane',
|
|
9
|
+
name: isNonEmptyString,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
2
|
+
import { isNonEmptyString } from '@ankhorage/utility/string';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
InfraComputeSelection,
|
|
6
|
+
InfraDeploymentSpec,
|
|
7
|
+
InfraRuntimeSelection,
|
|
8
|
+
} from '../types/infraManifest';
|
|
9
|
+
import type { InfraShape } from '../types/infraValidation';
|
|
10
|
+
import { INFRA_RUNTIME_COMPATIBILITY } from './constants';
|
|
11
|
+
import { infraFields } from './infraFields';
|
|
12
|
+
import { isInfraCredentialRef } from './isInfraCredentialRef';
|
|
13
|
+
import { isInfraShape } from './isInfraShape';
|
|
14
|
+
|
|
15
|
+
/*** Validate provider-specific config and the exact same compatibility map used by TypeScript. */
|
|
16
|
+
export function isInfraDeploymentSpec(value: unknown): value is InfraDeploymentSpec {
|
|
17
|
+
if (!isInfraShape(value, { compute: isCompute, runtime: isRuntime }) || !isRecord(value))
|
|
18
|
+
return false;
|
|
19
|
+
if (!isRecord(value.compute) || !isRecord(value.runtime)) return false;
|
|
20
|
+
const compute = value.compute.provider;
|
|
21
|
+
const runtime = value.runtime.provider;
|
|
22
|
+
return Object.entries(INFRA_RUNTIME_COMPATIBILITY).some(
|
|
23
|
+
([id, providers]) => id === runtime && providers.some((provider) => provider === compute),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*** Compute configuration is closed per provider; control-plane refs cannot be managed secrets. */
|
|
28
|
+
function isCompute(value: unknown): boolean {
|
|
29
|
+
if (!isRecord(value)) return false;
|
|
30
|
+
if (value.provider === 'local')
|
|
31
|
+
return isInfraShape(value, {
|
|
32
|
+
provider: (provider) => provider === 'local',
|
|
33
|
+
workingDirectory: infraFields.optionalText,
|
|
34
|
+
} satisfies InfraShape<InfraComputeSelection<'local'>>);
|
|
35
|
+
return isInfraShape(value, {
|
|
36
|
+
provider: (provider) => provider === 'hetzner',
|
|
37
|
+
location: isNonEmptyString,
|
|
38
|
+
serverType: infraFields.optionalText,
|
|
39
|
+
image: infraFields.optionalText,
|
|
40
|
+
credentials: (credentials) => credentials === undefined || isInfraCredentialRef(credentials),
|
|
41
|
+
} satisfies InfraShape<InfraComputeSelection<'hetzner'>>);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*** Runtime topology expresses host roles and never couples k3s to a compute vendor. */
|
|
45
|
+
function isRuntime(value: unknown): boolean {
|
|
46
|
+
if (!isRecord(value)) return false;
|
|
47
|
+
switch (value.provider) {
|
|
48
|
+
case 'minikube':
|
|
49
|
+
return isInfraShape(value, {
|
|
50
|
+
provider: (provider) => provider === 'minikube',
|
|
51
|
+
profile: infraFields.optionalText,
|
|
52
|
+
driver: (driver) => driver === undefined || driver === 'docker' || driver === 'podman',
|
|
53
|
+
cpus: infraFields.optionalPositiveInteger,
|
|
54
|
+
memoryMiB: infraFields.optionalPositiveInteger,
|
|
55
|
+
} satisfies InfraShape<InfraRuntimeSelection<'minikube'>>);
|
|
56
|
+
case 'k3s':
|
|
57
|
+
return isInfraShape(value, {
|
|
58
|
+
provider: (provider) => provider === 'k3s',
|
|
59
|
+
version: infraFields.optionalText,
|
|
60
|
+
topology: (topology) =>
|
|
61
|
+
topology === undefined ||
|
|
62
|
+
isInfraShape(topology, {
|
|
63
|
+
servers: infraFields.positiveInteger,
|
|
64
|
+
agents: infraFields.nonnegativeInteger,
|
|
65
|
+
}),
|
|
66
|
+
} satisfies InfraShape<InfraRuntimeSelection<'k3s'>>);
|
|
67
|
+
case 'docker-compose':
|
|
68
|
+
return isInfraShape(value, {
|
|
69
|
+
provider: (provider) => provider === 'docker-compose',
|
|
70
|
+
projectName: infraFields.optionalText,
|
|
71
|
+
} satisfies InfraShape<InfraRuntimeSelection<'docker-compose'>>);
|
|
72
|
+
default:
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
2
|
+
|
|
3
|
+
import type { InfraEnvironmentSpec, InfraObjectStorageSpec } from '../types/infraManifest';
|
|
4
|
+
import type { InfraShape } from '../types/infraValidation';
|
|
5
|
+
import { INFRA_ADAPTER_CATALOG } from './constants';
|
|
6
|
+
import { infraFields } from './infraFields';
|
|
7
|
+
import { isInfraAuthSpec } from './isInfraAuthSpec';
|
|
8
|
+
import { isInfraCredentialRef } from './isInfraCredentialRef';
|
|
9
|
+
import { isInfraDeploymentSpec } from './isInfraDeploymentSpec';
|
|
10
|
+
import { isInfraShape } from './isInfraShape';
|
|
11
|
+
import { isInfraWorkloadSpec } from './isInfraWorkloadSpec';
|
|
12
|
+
|
|
13
|
+
/*** Validate sibling capabilities and their required relationships within one environment. */
|
|
14
|
+
export function isInfraEnvironmentSpec(value: unknown): value is InfraEnvironmentSpec {
|
|
15
|
+
return isEnvironmentShape(value) && hasServiceDependencies(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/*** Check all environment field shapes before inspecting canonical dependency metadata. */
|
|
19
|
+
function isEnvironmentShape(value: unknown): value is InfraEnvironmentSpec {
|
|
20
|
+
return isInfraShape(value, {
|
|
21
|
+
deployment: isInfraDeploymentSpec,
|
|
22
|
+
database: (database) =>
|
|
23
|
+
database === undefined ||
|
|
24
|
+
isInfraShape(database, {
|
|
25
|
+
provider: (provider) => provider === 'supabase',
|
|
26
|
+
tier: (tier) => tier === undefined || tier === 'dev' || tier === 'prod',
|
|
27
|
+
}),
|
|
28
|
+
objectStorage: (storage) => storage === undefined || isObjectStorage(storage),
|
|
29
|
+
auth: (auth) => auth === undefined || isInfraAuthSpec(auth),
|
|
30
|
+
authz: (authz) =>
|
|
31
|
+
authz === undefined ||
|
|
32
|
+
isInfraShape(authz, {
|
|
33
|
+
provider: (provider) => provider === 'cerbos',
|
|
34
|
+
kind: (kind) => kind === 'ABAC' || kind === 'RBAC',
|
|
35
|
+
}),
|
|
36
|
+
secretStore: (store) =>
|
|
37
|
+
store === undefined ||
|
|
38
|
+
isInfraShape(store, {
|
|
39
|
+
provider: (provider) => provider === 'supabase-vault',
|
|
40
|
+
schema: infraFields.optionalText,
|
|
41
|
+
}),
|
|
42
|
+
networking: (networking) =>
|
|
43
|
+
networking === undefined || isInfraShape(networking, { domain: infraFields.optionalText }),
|
|
44
|
+
workloads: (workloads) => workloads === undefined || isWorkloads(workloads),
|
|
45
|
+
} satisfies InfraShape<InfraEnvironmentSpec>);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*** Object storage can vary independently from database/auth; there is no implicit auto provider. */
|
|
49
|
+
function isObjectStorage(value: unknown): boolean {
|
|
50
|
+
if (!isRecord(value)) return false;
|
|
51
|
+
const common = {
|
|
52
|
+
provider: (provider: unknown) => provider === value.provider,
|
|
53
|
+
buckets: infraFields.optionalStrings,
|
|
54
|
+
};
|
|
55
|
+
if (value.provider === 'supabase') return isInfraShape(value, common);
|
|
56
|
+
return isInfraShape(value, {
|
|
57
|
+
...common,
|
|
58
|
+
provider: (provider) => provider === 'r2',
|
|
59
|
+
accountId: infraFields.optionalText,
|
|
60
|
+
credentials: (credentials) => credentials === undefined || isInfraCredentialRef(credentials),
|
|
61
|
+
} satisfies InfraShape<Extract<InfraObjectStorageSpec, { readonly provider: 'r2' }>>);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/*** Duplicate desired workload identities cannot be reconciled safely. */
|
|
65
|
+
function isWorkloads(value: unknown): boolean {
|
|
66
|
+
return (
|
|
67
|
+
Array.isArray(value) &&
|
|
68
|
+
value.every(isInfraWorkloadSpec) &&
|
|
69
|
+
new Set(value.map((workload) => workload.id)).size === value.length
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*** Check catalog dependencies against selected sibling capabilities and runtime capabilities. */
|
|
74
|
+
function hasServiceDependencies(value: InfraEnvironmentSpec): boolean {
|
|
75
|
+
const selections = [
|
|
76
|
+
value.database,
|
|
77
|
+
value.auth,
|
|
78
|
+
value.authz,
|
|
79
|
+
value.objectStorage,
|
|
80
|
+
value.secretStore,
|
|
81
|
+
];
|
|
82
|
+
const runtime = Object.values(INFRA_ADAPTER_CATALOG).find(
|
|
83
|
+
(entry) => entry.id === value.deployment.runtime.provider,
|
|
84
|
+
);
|
|
85
|
+
const capabilities = new Set<string>([
|
|
86
|
+
'compute',
|
|
87
|
+
...(runtime?.capabilities ?? []),
|
|
88
|
+
...Object.entries(value)
|
|
89
|
+
.filter(([, selection]) => selection !== undefined)
|
|
90
|
+
.map(([capability]) => capability),
|
|
91
|
+
]);
|
|
92
|
+
return Object.values(INFRA_ADAPTER_CATALOG)
|
|
93
|
+
.filter((entry) => selections.some((selection) => selection?.provider === entry.id))
|
|
94
|
+
.every((entry) => entry.dependencies.every((dependency) => capabilities.has(dependency)));
|
|
95
|
+
}
|
|
@@ -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
|
+
}
|