@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
package/src/contracts.test.ts
CHANGED
|
@@ -9,11 +9,10 @@ import {
|
|
|
9
9
|
APP_CATEGORIES,
|
|
10
10
|
type AppCategory,
|
|
11
11
|
type AppManifest,
|
|
12
|
-
|
|
12
|
+
type AppStateSpec,
|
|
13
13
|
AUTH_SIGN_IN_IDENTIFIERS,
|
|
14
14
|
AUTH_SIGN_UP_POLICIES,
|
|
15
15
|
type AuthFlowConfig,
|
|
16
|
-
type AuthSpec,
|
|
17
16
|
type ButtonPressEventDto,
|
|
18
17
|
type CollectionItemPressEventDto,
|
|
19
18
|
type ComponentEventDto,
|
|
@@ -22,16 +21,12 @@ import {
|
|
|
22
21
|
type DbAdminAdapter,
|
|
23
22
|
type DbChangeEvent,
|
|
24
23
|
type DbRealtimeAdapter,
|
|
25
|
-
DEPLOYMENT_TARGETS,
|
|
26
24
|
type FormSubmitEventDto,
|
|
27
25
|
type ImageAssetSource,
|
|
26
|
+
type InfraAuthSpec,
|
|
28
27
|
type InfraManifest,
|
|
29
28
|
NAVIGATOR_TYPES,
|
|
30
29
|
type RouteDefinition,
|
|
31
|
-
type SplashScreenSpec,
|
|
32
|
-
STATE_PERSISTENCE_MODES,
|
|
33
|
-
STATE_PROVIDERS,
|
|
34
|
-
type StateSpec,
|
|
35
30
|
type StoragePublicUrlResult,
|
|
36
31
|
type StorageResult,
|
|
37
32
|
type StorageUploadResult,
|
|
@@ -106,10 +101,6 @@ describe('contracts', () => {
|
|
|
106
101
|
'utilities_tools',
|
|
107
102
|
'weather',
|
|
108
103
|
]);
|
|
109
|
-
expect(DEPLOYMENT_TARGETS).toEqual(['minikube']);
|
|
110
|
-
expect(AUTH_PROVIDERS).toEqual(['supabase']);
|
|
111
|
-
expect(STATE_PROVIDERS).toEqual(['legend']);
|
|
112
|
-
expect(STATE_PERSISTENCE_MODES).toEqual(['none', 'local', 'secure', 'database']);
|
|
113
104
|
});
|
|
114
105
|
|
|
115
106
|
it('exports the app category union for template packages', () => {
|
|
@@ -196,25 +187,6 @@ describe('contracts', () => {
|
|
|
196
187
|
expect(theme.recipes?.patterns?.Panel?.padding).toBe('xl');
|
|
197
188
|
});
|
|
198
189
|
|
|
199
|
-
it('accepts serializable splash screen branding on app manifests', () => {
|
|
200
|
-
const splashScreen: SplashScreenSpec = {
|
|
201
|
-
backgroundColor: '#ffffff',
|
|
202
|
-
image: { mediaId: 'splash-logo' },
|
|
203
|
-
imageWidth: 160,
|
|
204
|
-
resizeMode: 'contain',
|
|
205
|
-
dark: {
|
|
206
|
-
backgroundColor: '#000000',
|
|
207
|
-
image: { mediaId: 'splash-logo-dark' },
|
|
208
|
-
imageWidth: 160,
|
|
209
|
-
resizeMode: 'contain',
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const manifest: Pick<AppManifest, 'splashScreen'> = { splashScreen };
|
|
214
|
-
|
|
215
|
-
expect(JSON.parse(JSON.stringify(manifest))).toEqual({ splashScreen });
|
|
216
|
-
});
|
|
217
|
-
|
|
218
190
|
it('accepts screens with operation data loaders and repeat empty-state nodes', () => {
|
|
219
191
|
const manifest: Pick<AppManifest, 'screens'> = {
|
|
220
192
|
screens: {
|
|
@@ -275,14 +247,19 @@ describe('contracts', () => {
|
|
|
275
247
|
expect(manifest.screens.products?.root.repeat?.empty?.[0]?.type).toBe('Notice');
|
|
276
248
|
});
|
|
277
249
|
|
|
278
|
-
it('
|
|
279
|
-
const state:
|
|
250
|
+
it('selects in-memory application state independently from infrastructure', () => {
|
|
251
|
+
const state: AppStateSpec = {
|
|
280
252
|
provider: 'legend',
|
|
281
|
-
persistence:
|
|
253
|
+
persistence: false,
|
|
282
254
|
};
|
|
283
|
-
const manifest: Pick<AppManifest, 'infra'> = {
|
|
255
|
+
const manifest: Pick<AppManifest, 'infra' | 'state'> = {
|
|
256
|
+
state,
|
|
284
257
|
infra: {
|
|
285
|
-
|
|
258
|
+
environments: {
|
|
259
|
+
local: {
|
|
260
|
+
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
286
263
|
modules: [],
|
|
287
264
|
modulesConfig: {
|
|
288
265
|
localization: {
|
|
@@ -293,8 +270,9 @@ describe('contracts', () => {
|
|
|
293
270
|
};
|
|
294
271
|
|
|
295
272
|
expect(JSON.parse(JSON.stringify(manifest))).toEqual({
|
|
273
|
+
state,
|
|
296
274
|
infra: {
|
|
297
|
-
|
|
275
|
+
environments: manifest.infra.environments,
|
|
298
276
|
modules: [],
|
|
299
277
|
modulesConfig: {
|
|
300
278
|
localization: {
|
|
@@ -312,6 +290,11 @@ describe('contracts', () => {
|
|
|
312
290
|
const hasLegacyModules: HasLegacyModules = false;
|
|
313
291
|
const hasLegacyModulesConfig: HasLegacyModulesConfig = false;
|
|
314
292
|
const infra: InfraManifest = {
|
|
293
|
+
environments: {
|
|
294
|
+
local: {
|
|
295
|
+
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
296
|
+
},
|
|
297
|
+
},
|
|
315
298
|
modules: ['expo-localization'],
|
|
316
299
|
modulesConfig: {
|
|
317
300
|
'expo-localization': {
|
|
@@ -328,7 +311,7 @@ describe('contracts', () => {
|
|
|
328
311
|
|
|
329
312
|
expect(hasLegacyModules).toBe(false);
|
|
330
313
|
expect(hasLegacyModulesConfig).toBe(false);
|
|
331
|
-
expect(Object.keys(infra).sort()).toEqual(['modules', 'modulesConfig']);
|
|
314
|
+
expect(Object.keys(infra).sort()).toEqual(['environments', 'modules', 'modulesConfig']);
|
|
332
315
|
expect(expoConfig.plugins).toEqual(['expo-router']);
|
|
333
316
|
});
|
|
334
317
|
|
|
@@ -387,10 +370,9 @@ describe('contracts', () => {
|
|
|
387
370
|
unauthorizedRoute: '/sign-in',
|
|
388
371
|
};
|
|
389
372
|
|
|
390
|
-
const auth:
|
|
373
|
+
const auth: InfraAuthSpec = {
|
|
391
374
|
scope: 'global',
|
|
392
375
|
provider: 'supabase',
|
|
393
|
-
authorization: { kind: 'RBAC', engine: 'cerbos' },
|
|
394
376
|
flow: authFlow,
|
|
395
377
|
signIn: { identifiers: ['email'] },
|
|
396
378
|
signUp: {
|
package/src/deploy.test.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { describe, expect, it } from 'bun:test';
|
|
5
5
|
|
|
6
6
|
import { isAppManifest } from './appManifest';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { APP_DEPLOY_TARGET_IDS, isAppDeployManifest } from './deploy';
|
|
8
|
+
import { APP_ENVIRONMENT_IDS } from './environments';
|
|
9
|
+
import { INFRA_RUNTIME_COMPATIBILITY } from './infra';
|
|
9
10
|
|
|
10
11
|
function createManifest(deploy?: unknown): Record<string, unknown> {
|
|
11
12
|
return {
|
|
@@ -26,7 +27,14 @@ function createManifest(deploy?: unknown): Record<string, unknown> {
|
|
|
26
27
|
],
|
|
27
28
|
activeThemeId: 'default',
|
|
28
29
|
...(deploy === undefined ? {} : { deploy }),
|
|
29
|
-
infra: {
|
|
30
|
+
infra: {
|
|
31
|
+
environments: {
|
|
32
|
+
local: {
|
|
33
|
+
deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
modules: [],
|
|
37
|
+
},
|
|
30
38
|
navigator: { type: 'stack', routes: [] },
|
|
31
39
|
screens: {},
|
|
32
40
|
settings: { localization: { defaultLocale: 'en', locales: ['en'] } },
|
|
@@ -36,11 +44,11 @@ function createManifest(deploy?: unknown): Record<string, unknown> {
|
|
|
36
44
|
describe('app deployment contracts', () => {
|
|
37
45
|
it('keeps app distribution targets distinct from Infra deployment targets', () => {
|
|
38
46
|
expect(APP_DEPLOY_TARGET_IDS).toEqual(['web', 'android', 'ios']);
|
|
39
|
-
expect(
|
|
47
|
+
expect(Object.keys(INFRA_RUNTIME_COMPATIBILITY)).toEqual(['minikube', 'k3s', 'docker-compose']);
|
|
40
48
|
});
|
|
41
49
|
|
|
42
50
|
it('defines one logical environment vocabulary across platform-specific execution', () => {
|
|
43
|
-
expect(
|
|
51
|
+
expect(APP_ENVIRONMENT_IDS).toEqual(['local', 'preview', 'production']);
|
|
44
52
|
});
|
|
45
53
|
|
|
46
54
|
it('accepts canonical web, Android, and iOS desired state with stable native schemes', () => {
|
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
|
+
}
|