@ankhorage/contracts 14.0.0 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +10 -2
  3. package/dist/appManifest/apis.d.ts +1 -0
  4. package/dist/appManifest/apis.d.ts.map +1 -1
  5. package/dist/appManifest/apis.js +17 -14
  6. package/dist/appManifest/apis.js.map +1 -1
  7. package/dist/appManifest/bindings.d.ts +3 -0
  8. package/dist/appManifest/bindings.d.ts.map +1 -1
  9. package/dist/appManifest/bindings.js +17 -1
  10. package/dist/appManifest/bindings.js.map +1 -1
  11. package/dist/appManifest/constants.d.ts +18 -0
  12. package/dist/appManifest/constants.d.ts.map +1 -0
  13. package/dist/appManifest/constants.js +18 -0
  14. package/dist/appManifest/constants.js.map +1 -0
  15. package/dist/appManifest/data.d.ts +4 -0
  16. package/dist/appManifest/data.d.ts.map +1 -1
  17. package/dist/appManifest/data.js +22 -3
  18. package/dist/appManifest/data.js.map +1 -1
  19. package/dist/appManifest/dataSources.d.ts +1 -0
  20. package/dist/appManifest/dataSources.d.ts.map +1 -1
  21. package/dist/appManifest/dataSources.js +7 -6
  22. package/dist/appManifest/dataSources.js.map +1 -1
  23. package/dist/appManifest/deploy.d.ts +1 -0
  24. package/dist/appManifest/deploy.d.ts.map +1 -1
  25. package/dist/appManifest/deploy.js +16 -13
  26. package/dist/appManifest/deploy.js.map +1 -1
  27. package/dist/appManifest/icon.d.ts.map +1 -1
  28. package/dist/appManifest/icon.js +2 -1
  29. package/dist/appManifest/icon.js.map +1 -1
  30. package/dist/appManifest/isAppManifest.d.ts +4 -0
  31. package/dist/appManifest/isAppManifest.d.ts.map +1 -0
  32. package/dist/appManifest/isAppManifest.js +69 -0
  33. package/dist/appManifest/isAppManifest.js.map +1 -0
  34. package/dist/appManifest/isAppStateSpec.d.ts +4 -0
  35. package/dist/appManifest/isAppStateSpec.d.ts.map +1 -0
  36. package/dist/appManifest/isAppStateSpec.js +9 -0
  37. package/dist/appManifest/isAppStateSpec.js.map +1 -0
  38. package/dist/appManifest/isManifestValue.d.ts +3 -0
  39. package/dist/appManifest/isManifestValue.d.ts.map +1 -0
  40. package/dist/appManifest/isManifestValue.js +15 -0
  41. package/dist/appManifest/isManifestValue.js.map +1 -0
  42. package/dist/appManifest/media.d.ts +1 -0
  43. package/dist/appManifest/media.d.ts.map +1 -1
  44. package/dist/appManifest/media.js +10 -8
  45. package/dist/appManifest/media.js.map +1 -1
  46. package/dist/appManifest/navigator.d.ts.map +1 -1
  47. package/dist/appManifest/navigator.js +7 -6
  48. package/dist/appManifest/navigator.js.map +1 -1
  49. package/dist/appManifest/navigatorOptions.d.ts +0 -2
  50. package/dist/appManifest/navigatorOptions.d.ts.map +1 -1
  51. package/dist/appManifest/navigatorOptions.js +8 -12
  52. package/dist/appManifest/navigatorOptions.js.map +1 -1
  53. package/dist/appManifest/parseAppManifest.d.ts +10 -0
  54. package/dist/appManifest/parseAppManifest.d.ts.map +1 -0
  55. package/dist/appManifest/parseAppManifest.js +14 -0
  56. package/dist/appManifest/parseAppManifest.js.map +1 -0
  57. package/dist/appManifest/screens.d.ts +4 -0
  58. package/dist/appManifest/screens.d.ts.map +1 -1
  59. package/dist/appManifest/screens.js +14 -2
  60. package/dist/appManifest/screens.js.map +1 -1
  61. package/dist/appManifest.d.ts +3 -18
  62. package/dist/appManifest.d.ts.map +1 -1
  63. package/dist/appManifest.js +2 -80
  64. package/dist/appManifest.js.map +1 -1
  65. package/dist/auth.d.ts +3 -2
  66. package/dist/auth.d.ts.map +1 -1
  67. package/dist/auth.js.map +1 -1
  68. package/dist/deploy.d.ts +0 -9
  69. package/dist/deploy.d.ts.map +1 -1
  70. package/dist/deploy.js +0 -8
  71. package/dist/deploy.js.map +1 -1
  72. package/dist/environments.d.ts +4 -0
  73. package/dist/environments.d.ts.map +1 -0
  74. package/dist/environments.js +3 -0
  75. package/dist/environments.js.map +1 -0
  76. package/dist/index.d.ts +2 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/infra/constants.d.ts +91 -0
  81. package/dist/infra/constants.d.ts.map +1 -0
  82. package/dist/infra/constants.js +91 -0
  83. package/dist/infra/constants.js.map +1 -0
  84. package/dist/infra/infraFields.d.ts +12 -0
  85. package/dist/infra/infraFields.d.ts.map +1 -0
  86. package/dist/infra/infraFields.js +14 -0
  87. package/dist/infra/infraFields.js.map +1 -0
  88. package/dist/infra/isInfraAdapterDescriptor.d.ts +4 -0
  89. package/dist/infra/isInfraAdapterDescriptor.d.ts.map +1 -0
  90. package/dist/infra/isInfraAdapterDescriptor.js +31 -0
  91. package/dist/infra/isInfraAdapterDescriptor.js.map +1 -0
  92. package/dist/infra/isInfraAuthSpec.d.ts +4 -0
  93. package/dist/infra/isInfraAuthSpec.d.ts.map +1 -0
  94. package/dist/infra/isInfraAuthSpec.js +75 -0
  95. package/dist/infra/isInfraAuthSpec.js.map +1 -0
  96. package/dist/infra/isInfraCredentialRef.d.ts +3 -0
  97. package/dist/infra/isInfraCredentialRef.d.ts.map +1 -0
  98. package/dist/infra/isInfraCredentialRef.js +10 -0
  99. package/dist/infra/isInfraCredentialRef.js.map +1 -0
  100. package/dist/infra/isInfraDeploymentSpec.d.ts +4 -0
  101. package/dist/infra/isInfraDeploymentSpec.d.ts.map +1 -0
  102. package/dist/infra/isInfraDeploymentSpec.js +66 -0
  103. package/dist/infra/isInfraDeploymentSpec.js.map +1 -0
  104. package/dist/infra/isInfraEnvironmentSpec.d.ts +4 -0
  105. package/dist/infra/isInfraEnvironmentSpec.d.ts.map +1 -0
  106. package/dist/infra/isInfraEnvironmentSpec.js +82 -0
  107. package/dist/infra/isInfraEnvironmentSpec.js.map +1 -0
  108. package/dist/infra/isInfraManifest.d.ts +4 -0
  109. package/dist/infra/isInfraManifest.d.ts.map +1 -0
  110. package/dist/infra/isInfraManifest.js +23 -0
  111. package/dist/infra/isInfraManifest.js.map +1 -0
  112. package/dist/infra/isInfraShape.d.ts +3 -0
  113. package/dist/infra/isInfraShape.d.ts.map +1 -0
  114. package/dist/infra/isInfraShape.js +10 -0
  115. package/dist/infra/isInfraShape.js.map +1 -0
  116. package/dist/infra/isInfraWorkloadHealth.d.ts +3 -0
  117. package/dist/infra/isInfraWorkloadHealth.d.ts.map +1 -0
  118. package/dist/infra/isInfraWorkloadHealth.js +37 -0
  119. package/dist/infra/isInfraWorkloadHealth.js.map +1 -0
  120. package/dist/infra/isInfraWorkloadSpec.d.ts +4 -0
  121. package/dist/infra/isInfraWorkloadSpec.d.ts.map +1 -0
  122. package/dist/infra/isInfraWorkloadSpec.js +70 -0
  123. package/dist/infra/isInfraWorkloadSpec.js.map +1 -0
  124. package/dist/infra/isInfraWorkloadValue.d.ts +3 -0
  125. package/dist/infra/isInfraWorkloadValue.d.ts.map +1 -0
  126. package/dist/infra/isInfraWorkloadValue.js +40 -0
  127. package/dist/infra/isInfraWorkloadValue.js.map +1 -0
  128. package/dist/infra/parseInfraManifest.d.ts +5 -0
  129. package/dist/infra/parseInfraManifest.d.ts.map +1 -0
  130. package/dist/infra/parseInfraManifest.js +17 -0
  131. package/dist/infra/parseInfraManifest.js.map +1 -0
  132. package/dist/infra/validateInfraAdapterSelection.d.ts +6 -0
  133. package/dist/infra/validateInfraAdapterSelection.d.ts.map +1 -0
  134. package/dist/infra/validateInfraAdapterSelection.js +36 -0
  135. package/dist/infra/validateInfraAdapterSelection.js.map +1 -0
  136. package/dist/infra.d.ts +16 -0
  137. package/dist/infra.d.ts.map +1 -0
  138. package/dist/infra.js +11 -0
  139. package/dist/infra.js.map +1 -0
  140. package/dist/media.d.ts +7 -6
  141. package/dist/media.d.ts.map +1 -1
  142. package/dist/media.js +5 -5
  143. package/dist/media.js.map +1 -1
  144. package/dist/state.d.ts +5 -0
  145. package/dist/state.d.ts.map +1 -1
  146. package/dist/state.js.map +1 -1
  147. package/dist/types/appManifest.d.ts +9 -0
  148. package/dist/types/appManifest.d.ts.map +1 -0
  149. package/dist/types/appManifest.js +2 -0
  150. package/dist/types/appManifest.js.map +1 -0
  151. package/dist/types/infraAdapters.d.ts +85 -0
  152. package/dist/types/infraAdapters.d.ts.map +1 -0
  153. package/dist/types/infraAdapters.js +2 -0
  154. package/dist/types/infraAdapters.js.map +1 -0
  155. package/dist/types/infraLifecycle.d.ts +107 -0
  156. package/dist/types/infraLifecycle.d.ts.map +1 -0
  157. package/dist/types/infraLifecycle.js +2 -0
  158. package/dist/types/infraLifecycle.js.map +1 -0
  159. package/dist/types/infraManifest.d.ts +142 -0
  160. package/dist/types/infraManifest.d.ts.map +1 -0
  161. package/dist/types/infraManifest.js +2 -0
  162. package/dist/types/infraManifest.js.map +1 -0
  163. package/dist/types/infraSecrets.d.ts +15 -0
  164. package/dist/types/infraSecrets.d.ts.map +1 -0
  165. package/dist/types/infraSecrets.js +2 -0
  166. package/dist/types/infraSecrets.js.map +1 -0
  167. package/dist/types/infraValidation.d.ts +5 -0
  168. package/dist/types/infraValidation.d.ts.map +1 -0
  169. package/dist/types/infraValidation.js +2 -0
  170. package/dist/types/infraValidation.js.map +1 -0
  171. package/dist/types/infraWorkload.d.ts +70 -0
  172. package/dist/types/infraWorkload.d.ts.map +1 -0
  173. package/dist/types/infraWorkload.js +2 -0
  174. package/dist/types/infraWorkload.js.map +1 -0
  175. package/dist/types.d.ts +7 -70
  176. package/dist/types.d.ts.map +1 -1
  177. package/dist/types.js +0 -9
  178. package/dist/types.js.map +1 -1
  179. package/package.json +11 -2
  180. package/src/appManifest/apis.ts +18 -16
  181. package/src/appManifest/bindings.ts +18 -1
  182. package/src/appManifest/constants.ts +19 -0
  183. package/src/appManifest/data.ts +23 -9
  184. package/src/appManifest/dataSources.ts +8 -7
  185. package/src/appManifest/deploy.ts +17 -15
  186. package/src/appManifest/icon.ts +3 -1
  187. package/src/appManifest/isAppManifest.ts +95 -0
  188. package/src/appManifest/isAppStateSpec.ts +13 -0
  189. package/src/appManifest/isManifestValue.ts +19 -0
  190. package/src/appManifest/media.ts +11 -10
  191. package/src/appManifest/navigator.ts +7 -6
  192. package/src/appManifest/navigatorOptions.ts +9 -16
  193. package/src/appManifest/parseAppManifest.ts +15 -0
  194. package/src/appManifest/screens.ts +14 -2
  195. package/src/appManifest.test.ts +9 -5
  196. package/src/appManifest.ts +3 -107
  197. package/src/auth-flow-contract.test.ts +6 -10
  198. package/src/auth-oauth.test.ts +2 -2
  199. package/src/auth.ts +3 -2
  200. package/src/bindings.test.ts +5 -0
  201. package/src/contracts.test.ts +21 -19
  202. package/src/deploy.test.ts +13 -5
  203. package/src/deploy.ts +0 -11
  204. package/src/environments.ts +4 -0
  205. package/src/index.ts +2 -1
  206. package/src/infra/constants.ts +91 -0
  207. package/src/infra/infraFields.ts +20 -0
  208. package/src/infra/isInfraAdapterDescriptor.ts +37 -0
  209. package/src/infra/isInfraAuthSpec.ts +98 -0
  210. package/src/infra/isInfraCredentialRef.ts +11 -0
  211. package/src/infra/isInfraDeploymentSpec.ts +75 -0
  212. package/src/infra/isInfraEnvironmentSpec.ts +95 -0
  213. package/src/infra/isInfraManifest.ts +29 -0
  214. package/src/infra/isInfraShape.ts +16 -0
  215. package/src/infra/isInfraWorkloadHealth.ts +38 -0
  216. package/src/infra/isInfraWorkloadSpec.ts +87 -0
  217. package/src/infra/isInfraWorkloadValue.ts +41 -0
  218. package/src/infra/parseInfraManifest.ts +20 -0
  219. package/src/infra/validateInfraAdapterSelection.ts +42 -0
  220. package/src/infra-types.test.ts +78 -0
  221. package/src/infra-workload.test.ts +186 -0
  222. package/src/infra.test.ts +268 -0
  223. package/src/infra.ts +15 -0
  224. package/src/media.ts +12 -11
  225. package/src/profile-contract.test.ts +2 -3
  226. package/src/secrets.test.ts +19 -13
  227. package/src/state.ts +6 -0
  228. package/src/types/appManifest.ts +5 -0
  229. package/src/types/infraAdapters.ts +148 -0
  230. package/src/types/infraLifecycle.ts +105 -0
  231. package/src/types/infraManifest.ts +130 -0
  232. package/src/types/infraSecrets.ts +16 -0
  233. package/src/types/infraValidation.ts +2 -0
  234. package/src/types/infraWorkload.ts +64 -0
  235. package/src/types.ts +7 -87
  236. package/dist/appManifest/infra.d.ts +0 -2
  237. package/dist/appManifest/infra.d.ts.map +0 -1
  238. package/dist/appManifest/infra.js +0 -130
  239. package/dist/appManifest/infra.js.map +0 -1
  240. package/dist/appManifest/shared.d.ts +0 -8
  241. package/dist/appManifest/shared.d.ts.map +0 -1
  242. package/dist/appManifest/shared.js +0 -31
  243. package/dist/appManifest/shared.js.map +0 -1
  244. package/dist/secretManifest.d.ts +0 -11
  245. package/dist/secretManifest.d.ts.map +0 -1
  246. package/dist/secretManifest.js +0 -2
  247. package/dist/secretManifest.js.map +0 -1
  248. package/src/appManifest/infra.ts +0 -190
  249. package/src/appManifest/shared.ts +0 -40
  250. package/src/secretManifest.ts +0 -12
@@ -1,3 +1,6 @@
1
+ import { hasOnlyKeys, isRecord } from '@ankhorage/utility/object';
2
+ import { isNonEmptyString, isOptionalString } from '@ankhorage/utility/string';
3
+
1
4
  import {
2
5
  MEDIA_ASSET_KINDS,
3
6
  type MediaAsset,
@@ -5,10 +8,10 @@ import {
5
8
  type MediaAssetSource,
6
9
  type MediaManifest,
7
10
  } from '../media';
8
- import { isOptionalString, isRecord } from './shared';
9
11
 
10
12
  const MEDIA_ASSET_KIND_SET = new Set<string>(MEDIA_ASSET_KINDS);
11
13
 
14
+ /*** Validate authored media assets and their registry identity. */
12
15
  export function isMediaManifest(value: unknown): value is MediaManifest {
13
16
  if (!isRecord(value) || !hasOnlyKeys(value, ['assets']) || !isRecord(value.assets)) return false;
14
17
 
@@ -17,6 +20,7 @@ export function isMediaManifest(value: unknown): value is MediaManifest {
17
20
  );
18
21
  }
19
22
 
23
+ /*** Validate media identity, kind, source and optional metadata. */
20
24
  function isMediaAsset(value: unknown): value is MediaAsset {
21
25
  return (
22
26
  isRecord(value) &&
@@ -31,6 +35,7 @@ function isMediaAsset(value: unknown): value is MediaAsset {
31
35
  );
32
36
  }
33
37
 
38
+ /*** Validate the selected bundled, URL or storage-backed media source. */
34
39
  function isMediaAssetSource(value: unknown): value is MediaAssetSource {
35
40
  if (!isRecord(value) || typeof value.kind !== 'string') return false;
36
41
 
@@ -52,6 +57,7 @@ function isMediaAssetSource(value: unknown): value is MediaAssetSource {
52
57
  );
53
58
  }
54
59
 
60
+ /*** Validate optional descriptive and numeric media metadata. */
55
61
  function isMediaAssetMetadata(value: unknown): value is MediaAssetMetadata {
56
62
  return (
57
63
  isRecord(value) &&
@@ -72,10 +78,12 @@ function isMediaAssetMetadata(value: unknown): value is MediaAssetMetadata {
72
78
  );
73
79
  }
74
80
 
81
+ /*** Validate an authored HTTP or HTTPS media URL. */
75
82
  function isStableRemoteUrl(value: unknown): boolean {
76
83
  return typeof value === 'string' && /^https?:\/\//iu.test(value.trim());
77
84
  }
78
85
 
86
+ /*** Validate a portable bundled-media path. */
79
87
  function isBundledPath(value: unknown): boolean {
80
88
  if (!isNonEmptyString(value)) return false;
81
89
  const path = value.trim();
@@ -83,19 +91,12 @@ function isBundledPath(value: unknown): boolean {
83
91
  return !path.split('/').includes('..');
84
92
  }
85
93
 
86
- function isNonEmptyString(value: unknown): value is string {
87
- return typeof value === 'string' && value.trim().length > 0;
88
- }
89
-
94
+ /*** Accept an omitted media metric or a finite non-negative number. */
90
95
  function isOptionalFiniteNonNegativeNumber(value: unknown): boolean {
91
96
  return value === undefined || (typeof value === 'number' && Number.isFinite(value) && value >= 0);
92
97
  }
93
98
 
99
+ /*** Accept an omitted media metric or a finite positive number. */
94
100
  function isOptionalFinitePositiveNumber(value: unknown): boolean {
95
101
  return value === undefined || (typeof value === 'number' && Number.isFinite(value) && value > 0);
96
102
  }
97
-
98
- function hasOnlyKeys(value: Record<string, unknown>, allowedKeys: readonly string[]): boolean {
99
- const allowed = new Set(allowedKeys);
100
- return Object.keys(value).every((key) => allowed.has(key));
101
- }
@@ -1,14 +1,16 @@
1
+ import { isStringArray } from '@ankhorage/utility/array';
2
+ import { hasOnlyKeys, isRecord } from '@ankhorage/utility/object';
3
+ import { isOptionalString } from '@ankhorage/utility/string';
4
+
1
5
  import { type AppNavigatorManifest, NAVIGATOR_PRESETS, NAVIGATOR_TYPES } from '../navigator';
2
6
  import { isIconSpec } from './icon';
3
7
  import {
4
- hasOnlyKeys,
5
8
  isDrawerNavigatorOptions,
6
9
  isNavigatorScreenReference,
7
10
  isStackImplementationConfig,
8
11
  isStackScreenOptions,
9
12
  isTabsImplementationConfig,
10
13
  } from './navigatorOptions';
11
- import { isOptionalBoolean, isOptionalString, isRecord, isStringArray } from './shared';
12
14
 
13
15
  /*** Validate the complete serialized `AppManifest.navigator` slice. */
14
16
  export function isAppNavigatorManifest(value: unknown): value is AppNavigatorManifest {
@@ -72,12 +74,11 @@ function isRouteDefinition(value: unknown): boolean {
72
74
  return (
73
75
  isRecord(value) &&
74
76
  typeof value.name === 'string' &&
75
- isOptionalString(value.path) &&
76
- isOptionalString(value.label) &&
77
+ [value.path, value.label, value.screenId].every(isOptionalString) &&
77
78
  (value.icon === undefined || isIconSpec(value.icon)) &&
78
- isOptionalBoolean(value.showInPrimaryNavigation) &&
79
+ (value.showInPrimaryNavigation === undefined ||
80
+ typeof value.showInPrimaryNavigation === 'boolean') &&
79
81
  (value.guards === undefined || isStringArray(value.guards)) &&
80
- isOptionalString(value.screenId) &&
81
82
  (value.stackOptions === undefined || isStackScreenOptions(value.stackOptions)) &&
82
83
  (value.navigator === undefined || isNavigatorNode(value.navigator))
83
84
  );
@@ -1,3 +1,6 @@
1
+ import { hasOnlyKeys, isRecord } from '@ankhorage/utility/object';
2
+ import { isOptionalString } from '@ankhorage/utility/string';
3
+
1
4
  import {
2
5
  DRAWER_POSITIONS,
3
6
  DRAWER_TYPES,
@@ -8,7 +11,6 @@ import {
8
11
  NATIVE_TABS_MINIMIZE_BEHAVIORS,
9
12
  STACK_PRESENTATIONS,
10
13
  } from '../navigator';
11
- import { isOptionalBoolean, isOptionalString, isRecord } from './shared';
12
14
 
13
15
  /*** Validate stable native, JavaScript, or alpha Experimental Stack desired state. */
14
16
  export function isStackImplementationConfig(value: Record<string, unknown>): boolean {
@@ -63,7 +65,7 @@ export function isStackScreenOptions(value: unknown): boolean {
63
65
  }
64
66
  return (
65
67
  (value.sheetAllowedDetents === undefined || isSheetAllowedDetents(value.sheetAllowedDetents)) &&
66
- isOptionalBoolean(value.sheetGrabberVisible)
68
+ (value.sheetGrabberVisible === undefined || typeof value.sheetGrabberVisible === 'boolean')
67
69
  );
68
70
  }
69
71
 
@@ -77,8 +79,8 @@ export function isDrawerNavigatorOptions(value: unknown): boolean {
77
79
  contains(DRAWER_POSITIONS, value.drawerPosition))) &&
78
80
  (value.drawerType === undefined ||
79
81
  (typeof value.drawerType === 'string' && contains(DRAWER_TYPES, value.drawerType))) &&
80
- isOptionalBoolean(value.swipeEnabled) &&
81
- isOptionalBoolean(value.headerShown)
82
+ (value.swipeEnabled === undefined || typeof value.swipeEnabled === 'boolean') &&
83
+ (value.headerShown === undefined || typeof value.headerShown === 'boolean')
82
84
  );
83
85
  }
84
86
 
@@ -101,15 +103,6 @@ export function isNavigatorScreenReference(value: unknown): boolean {
101
103
  return isRecord(value) && hasOnlyKeys(value, ['screenId']) && typeof value.screenId === 'string';
102
104
  }
103
105
 
104
- /*** Check that a finite configuration object contains no unsupported keys. */
105
- export function hasOnlyKeys(
106
- value: Record<string, unknown>,
107
- allowedKeys: readonly string[],
108
- ): boolean {
109
- const allowed = new Set(allowedKeys);
110
- return Object.keys(value).every((key) => allowed.has(key));
111
- }
112
-
113
106
  /*** Validate JavaScript Stack options without accepting native-only presentations. */
114
107
  function isJavaScriptStackScreenOptions(value: unknown): boolean {
115
108
  return (
@@ -141,9 +134,9 @@ function isStackHeaderOptions(value: unknown): boolean {
141
134
  function isStackHeaderOptionsShape(value: Record<string, unknown>): boolean {
142
135
  return (
143
136
  isOptionalString(value.title) &&
144
- isOptionalBoolean(value.headerShown) &&
145
- isOptionalBoolean(value.headerTransparent) &&
146
- isOptionalBoolean(value.headerBackVisible)
137
+ (value.headerShown === undefined || typeof value.headerShown === 'boolean') &&
138
+ (value.headerTransparent === undefined || typeof value.headerTransparent === 'boolean') &&
139
+ (value.headerBackVisible === undefined || typeof value.headerBackVisible === 'boolean')
147
140
  );
148
141
  }
149
142
 
@@ -0,0 +1,15 @@
1
+ import type { AppManifestParseResult } from '../types/appManifest';
2
+ import { isAppManifest } from './isAppManifest';
3
+
4
+ /**
5
+ * Parse unknown JSON-compatible input at the canonical AppManifest boundary.
6
+ *
7
+ * Contracts owns structural manifest validation. Consumers may add semantic
8
+ * diagnostics after this parser succeeds, but should not reconstruct the
9
+ * AppManifest shape in their own packages.
10
+ */
11
+ export function parseAppManifest(value: unknown): AppManifestParseResult {
12
+ return isAppManifest(value)
13
+ ? { ok: true, manifest: value }
14
+ : { ok: false, message: 'Value is not a canonical AppManifest.' };
15
+ }
@@ -1,9 +1,10 @@
1
1
  import { COLOR_HARMONIES } from '@ankhorage/color-theory';
2
+ import { isRecord } from '@ankhorage/utility/object';
3
+ import { isOptionalString } from '@ankhorage/utility/string';
2
4
 
3
5
  import { isMediaAssetReference } from '../media';
4
6
  import { APP_CATEGORIES } from '../types';
5
7
  import { isBindingValueSource, isScreenDataLoaderDefinition } from './bindings';
6
- import { isOptionalNumber, isOptionalString, isRecord } from './shared';
7
8
 
8
9
  export { isAppNavigatorManifest } from './navigator';
9
10
 
@@ -11,6 +12,7 @@ const APP_CATEGORY_SET = new Set<string>(APP_CATEGORIES);
11
12
  const COLOR_HARMONY_SET = new Set<string>(COLOR_HARMONIES);
12
13
  const SPLASH_SCREEN_RESIZE_MODE_SET = new Set<string>(['contain', 'cover', 'native']);
13
14
 
15
+ /*** Validate application identity and optional authored metadata. */
14
16
  export function isManifestMetadata(value: unknown): boolean {
15
17
  return (
16
18
  isRecord(value) &&
@@ -25,6 +27,7 @@ export function isManifestMetadata(value: unknown): boolean {
25
27
  );
26
28
  }
27
29
 
30
+ /*** Validate theme identity and its required light and dark modes. */
28
31
  export function isThemeConfig(value: unknown): boolean {
29
32
  return (
30
33
  isRecord(value) &&
@@ -35,6 +38,7 @@ export function isThemeConfig(value: unknown): boolean {
35
38
  );
36
39
  }
37
40
 
41
+ /*** Validate screens in the authored screen registry. */
38
42
  export function isScreenRegistry(value: unknown): boolean {
39
43
  return (
40
44
  isRecord(value) &&
@@ -45,6 +49,7 @@ export function isScreenRegistry(value: unknown): boolean {
45
49
  );
46
50
  }
47
51
 
52
+ /*** Validate optional light and dark splash-screen configuration. */
48
53
  export function isSplashScreenSpec(value: unknown): boolean {
49
54
  return (
50
55
  isSplashScreenModeSpec(value) &&
@@ -53,6 +58,7 @@ export function isSplashScreenSpec(value: unknown): boolean {
53
58
  );
54
59
  }
55
60
 
61
+ /*** Validate the primary color and supported color harmony of a theme mode. */
56
62
  function isThemeModeConfig(value: unknown): boolean {
57
63
  return (
58
64
  isRecord(value) &&
@@ -62,6 +68,7 @@ function isThemeModeConfig(value: unknown): boolean {
62
68
  );
63
69
  }
64
70
 
71
+ /*** Validate an authored component node and its nested children. */
65
72
  function isUiNode(value: unknown): boolean {
66
73
  return (
67
74
  isRecord(value) &&
@@ -76,6 +83,7 @@ function isUiNode(value: unknown): boolean {
76
83
  );
77
84
  }
78
85
 
86
+ /*** Validate repeat source and item binding configuration. */
79
87
  function isUiNodeRepeatSpec(value: unknown): boolean {
80
88
  return (
81
89
  isRecord(value) &&
@@ -86,6 +94,7 @@ function isUiNodeRepeatSpec(value: unknown): boolean {
86
94
  );
87
95
  }
88
96
 
97
+ /*** Validate screen identity, root component and optional requirements. */
89
98
  function isScreenSpec(value: unknown): boolean {
90
99
  return (
91
100
  isRecord(value) &&
@@ -101,11 +110,12 @@ function isScreenSpec(value: unknown): boolean {
101
110
  );
102
111
  }
103
112
 
113
+ /*** Validate one splash-screen appearance configuration. */
104
114
  function isSplashScreenModeSpec(value: unknown): boolean {
105
115
  return (
106
116
  isRecord(value) &&
107
117
  (value.image === undefined || isMediaAssetReference(value.image)) &&
108
- isOptionalNumber(value.imageWidth) &&
118
+ (value.imageWidth === undefined || typeof value.imageWidth === 'number') &&
109
119
  (value.resizeMode === undefined ||
110
120
  (typeof value.resizeMode === 'string' &&
111
121
  SPLASH_SCREEN_RESIZE_MODE_SET.has(value.resizeMode))) &&
@@ -113,6 +123,7 @@ function isSplashScreenModeSpec(value: unknown): boolean {
113
123
  );
114
124
  }
115
125
 
126
+ /*** Validate screen capability and permission requirements. */
116
127
  function isScreenRequirements(value: unknown): boolean {
117
128
  return (
118
129
  isRecord(value) &&
@@ -121,6 +132,7 @@ function isScreenRequirements(value: unknown): boolean {
121
132
  );
122
133
  }
123
134
 
135
+ /*** Validate the entries of a capability or permission requirement list. */
124
136
  function isRequirementArray(value: unknown, key: 'capability' | 'permission'): boolean {
125
137
  return (
126
138
  Array.isArray(value) &&
@@ -37,12 +37,16 @@ function createManifest(): Record<string, unknown> {
37
37
  },
38
38
  },
39
39
  },
40
+ state: { provider: 'legend', persistence: false },
40
41
  infra: {
41
- deployment: { target: 'minikube', monitoring: true },
42
- database: { provider: 'supabase', tier: 'dev' },
43
- storage: { provider: 'auto', buckets: ['media'] },
44
- state: { provider: 'legend', persistence: 'local' },
45
- networking: { domain: 'example.test', cdn: false },
42
+ environments: {
43
+ local: {
44
+ deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
45
+ database: { provider: 'supabase', tier: 'dev' },
46
+ objectStorage: { provider: 'supabase', buckets: ['media'] },
47
+ networking: { domain: 'example.test' },
48
+ },
49
+ },
46
50
  apis: [
47
51
  {
48
52
  id: 'nutrition',
@@ -1,112 +1,8 @@
1
- import { isComponentDataBindingRegistry } from './appManifest/bindings';
2
- import { isDataSourceRegistry } from './appManifest/dataSources';
3
- import { isAppDeployManifest } from './appManifest/deploy';
4
- import { isInfraManifest } from './appManifest/infra';
5
- import { isMediaManifest } from './appManifest/media';
6
- import {
7
- isAppNavigatorManifest,
8
- isManifestMetadata,
9
- isScreenRegistry,
10
- isSplashScreenSpec,
11
- isThemeConfig,
12
- } from './appManifest/screens';
13
- import { isRecord, isStringArray } from './appManifest/shared';
14
- import type { AppManifest } from './types';
15
-
1
+ export { isAppManifest } from './appManifest/isAppManifest';
2
+ export { parseAppManifest } from './appManifest/parseAppManifest';
16
3
  export type {
17
4
  SplashScreenModeSpec,
18
5
  SplashScreenResizeMode,
19
6
  SplashScreenSpec,
20
7
  } from './appManifest/splashScreen';
21
-
22
- export type AppManifestParseResult =
23
- | { readonly ok: true; readonly manifest: AppManifest }
24
- | { readonly ok: false; readonly message: string };
25
-
26
- const APP_MANIFEST_KEY_POLICY = {
27
- metadata: 'required',
28
- themes: 'required',
29
- activeThemeId: 'required',
30
- activeThemeMode: 'optional',
31
- splashScreen: 'optional',
32
- media: 'optional',
33
- deploy: 'optional',
34
- infra: 'required',
35
- navigator: 'required',
36
- screens: 'required',
37
- dataSources: 'optional',
38
- dataBindings: 'optional',
39
- repository: 'optional',
40
- settings: 'required',
41
- } as const satisfies Record<keyof AppManifest, 'optional' | 'required'>;
42
-
43
- /**
44
- * Parse unknown JSON-compatible input at the canonical AppManifest boundary.
45
- *
46
- * Contracts owns structural manifest validation. Consumers may add semantic
47
- * diagnostics after this parser succeeds, but should not reconstruct the
48
- * AppManifest shape in their own packages.
49
- */
50
- export function parseAppManifest(value: unknown): AppManifestParseResult {
51
- return isAppManifest(value)
52
- ? { ok: true, manifest: value }
53
- : { ok: false, message: 'Value is not a canonical AppManifest.' };
54
- }
55
-
56
- /** Return whether an unknown value satisfies the canonical AppManifest shape. */
57
- export function isAppManifest(value: unknown): value is AppManifest {
58
- return (
59
- isRecord(value) &&
60
- hasRequiredManifestKeys(value) &&
61
- !('generatedApis' in value) &&
62
- isManifestMetadata(value.metadata) &&
63
- Array.isArray(value.themes) &&
64
- value.themes.every(isThemeConfig) &&
65
- typeof value.activeThemeId === 'string' &&
66
- isActiveThemeMode(value.activeThemeMode) &&
67
- (value.splashScreen === undefined || isSplashScreenSpec(value.splashScreen)) &&
68
- (value.media === undefined || isMediaManifest(value.media)) &&
69
- (value.deploy === undefined || isAppDeployManifest(value.deploy)) &&
70
- isInfraManifest(value.infra) &&
71
- isAppNavigatorManifest(value.navigator) &&
72
- isScreenRegistry(value.screens) &&
73
- (value.dataSources === undefined || isDataSourceRegistry(value.dataSources)) &&
74
- (value.dataBindings === undefined || isComponentDataBindingRegistry(value.dataBindings)) &&
75
- (value.repository === undefined || isRepositoryManifest(value.repository)) &&
76
- isAppSettings(value.settings)
77
- );
78
- }
79
-
80
- function hasRequiredManifestKeys(value: Record<string, unknown>): boolean {
81
- return Object.entries(APP_MANIFEST_KEY_POLICY).every(
82
- ([key, policy]) => policy === 'optional' || key in value,
83
- );
84
- }
85
-
86
- function isActiveThemeMode(value: unknown): boolean {
87
- return value === undefined || value === 'dark' || value === 'light';
88
- }
89
-
90
- function isRepositoryManifest(value: unknown): boolean {
91
- return (
92
- isRecord(value) &&
93
- value.provider === 'github' &&
94
- typeof value.owner === 'string' &&
95
- value.owner.length > 0 &&
96
- typeof value.name === 'string' &&
97
- value.name.length > 0 &&
98
- typeof value.url === 'string' &&
99
- value.url.length > 0 &&
100
- value.defaultBranch === 'main'
101
- );
102
- }
103
-
104
- function isAppSettings(value: unknown): boolean {
105
- return (
106
- isRecord(value) &&
107
- !('apiBaseUrl' in value) &&
108
- isRecord(value.localization) &&
109
- typeof value.localization.defaultLocale === 'string' &&
110
- isStringArray(value.localization.locales)
111
- );
112
- }
8
+ export type { AppManifestParseResult } from './types/appManifest';
@@ -7,8 +7,8 @@ import {
7
7
  type AppSettings,
8
8
  type AuthFlowConfig,
9
9
  type AuthProviderConfig,
10
- type AuthSpec,
11
10
  DEFAULT_AUTH_FLOW,
11
+ type InfraAuthSpec,
12
12
  resolveAuthFlow,
13
13
  } from './index';
14
14
 
@@ -52,13 +52,13 @@ describe('canonical authentication flow contract', () => {
52
52
  });
53
53
 
54
54
  it('allows authentication without an authorization model', () => {
55
- const auth: AuthSpec = {
55
+ const auth: InfraAuthSpec = {
56
56
  scope: 'global',
57
57
  provider: 'supabase',
58
58
  flow: DEFAULT_AUTH_FLOW,
59
59
  };
60
60
 
61
- expect(auth.authorization).toBeUndefined();
61
+ expect(Object.hasOwn(auth, 'authorization')).toBe(false);
62
62
  });
63
63
 
64
64
  it('removes auth flow from application settings at compile time', () => {
@@ -91,13 +91,9 @@ describe('canonical authentication flow contract', () => {
91
91
  expect(removedProperty.provider).toBe('supabase');
92
92
  });
93
93
 
94
- it('makes authorization access require explicit narrowing', () => {
95
- const assumesAuthorization = (auth: AuthSpec): string => {
96
- // @ts-expect-error Authorization is optional and must be checked before use.
97
- return auth.authorization.kind;
98
- };
99
-
100
- expect(typeof assumesAuthorization).toBe('function');
94
+ it('removes nested authorization from the authentication contract', () => {
95
+ const hasAuthorization: 'authorization' extends keyof InfraAuthSpec ? true : false = false;
96
+ expect(hasAuthorization).toBe(false);
101
97
  });
102
98
 
103
99
  it('prevents the removed settings auth-flow path from returning to source', async () => {
@@ -11,7 +11,7 @@ import {
11
11
  type AuthAdapter,
12
12
  type AuthOAuthConfig,
13
13
  type AuthSession,
14
- type AuthSpec,
14
+ type InfraAuthSpec,
15
15
  } from './index';
16
16
 
17
17
  const session: AuthSession = {
@@ -80,7 +80,7 @@ describe('OAuth auth contracts', () => {
80
80
  ],
81
81
  };
82
82
 
83
- const auth: AuthSpec = {
83
+ const auth: InfraAuthSpec = {
84
84
  scope: 'global',
85
85
  provider: 'supabase',
86
86
  oauth,
package/src/auth.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { AppDeployEnvironmentId, AppDeployTargetId } from './deploy';
1
+ import type { AppDeployTargetId } from './deploy';
2
+ import type { AppEnvironmentId } from './environments';
2
3
  import type { SecretRef } from './secrets';
3
4
  import type { IconSpec } from './types';
4
5
 
@@ -90,7 +91,7 @@ export type AuthOAuthSetupRequirement =
90
91
  export interface AuthOAuthSetupPlan {
91
92
  readonly provider: AuthOAuthProviderId;
92
93
  readonly transport: AuthOAuthTransportId;
93
- readonly environment: AppDeployEnvironmentId;
94
+ readonly environment: AppEnvironmentId;
94
95
  /** Enabled application targets considered while deriving this plan. */
95
96
  readonly targets: readonly AppDeployTargetId[];
96
97
  readonly requirements: readonly AuthOAuthSetupRequirement[];
@@ -490,6 +490,11 @@ describe('component data-binding contracts', () => {
490
490
  ],
491
491
  activeThemeId: 'default',
492
492
  infra: {
493
+ environments: {
494
+ local: {
495
+ deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
496
+ },
497
+ },
493
498
  apis: [
494
499
  {
495
500
  id: 'cms',
@@ -9,11 +9,10 @@ import {
9
9
  APP_CATEGORIES,
10
10
  type AppCategory,
11
11
  type AppManifest,
12
- AUTH_PROVIDERS,
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,15 +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
- STATE_PERSISTENCE_MODES,
32
- STATE_PROVIDERS,
33
- type StateSpec,
34
30
  type StoragePublicUrlResult,
35
31
  type StorageResult,
36
32
  type StorageUploadResult,
@@ -105,10 +101,6 @@ describe('contracts', () => {
105
101
  'utilities_tools',
106
102
  'weather',
107
103
  ]);
108
- expect(DEPLOYMENT_TARGETS).toEqual(['minikube']);
109
- expect(AUTH_PROVIDERS).toEqual(['supabase']);
110
- expect(STATE_PROVIDERS).toEqual(['legend']);
111
- expect(STATE_PERSISTENCE_MODES).toEqual(['none', 'local', 'secure', 'database']);
112
104
  });
113
105
 
114
106
  it('exports the app category union for template packages', () => {
@@ -255,14 +247,19 @@ describe('contracts', () => {
255
247
  expect(manifest.screens.products?.root.repeat?.empty?.[0]?.type).toBe('Notice');
256
248
  });
257
249
 
258
- it('accepts provider-neutral state infra selection on app manifests', () => {
259
- const state: StateSpec = {
250
+ it('selects in-memory application state independently from infrastructure', () => {
251
+ const state: AppStateSpec = {
260
252
  provider: 'legend',
261
- persistence: 'none',
253
+ persistence: false,
262
254
  };
263
- const manifest: Pick<AppManifest, 'infra'> = {
255
+ const manifest: Pick<AppManifest, 'infra' | 'state'> = {
256
+ state,
264
257
  infra: {
265
- state,
258
+ environments: {
259
+ local: {
260
+ deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
261
+ },
262
+ },
266
263
  modules: [],
267
264
  modulesConfig: {
268
265
  localization: {
@@ -273,8 +270,9 @@ describe('contracts', () => {
273
270
  };
274
271
 
275
272
  expect(JSON.parse(JSON.stringify(manifest))).toEqual({
273
+ state,
276
274
  infra: {
277
- state,
275
+ environments: manifest.infra.environments,
278
276
  modules: [],
279
277
  modulesConfig: {
280
278
  localization: {
@@ -292,6 +290,11 @@ describe('contracts', () => {
292
290
  const hasLegacyModules: HasLegacyModules = false;
293
291
  const hasLegacyModulesConfig: HasLegacyModulesConfig = false;
294
292
  const infra: InfraManifest = {
293
+ environments: {
294
+ local: {
295
+ deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
296
+ },
297
+ },
295
298
  modules: ['expo-localization'],
296
299
  modulesConfig: {
297
300
  'expo-localization': {
@@ -308,7 +311,7 @@ describe('contracts', () => {
308
311
 
309
312
  expect(hasLegacyModules).toBe(false);
310
313
  expect(hasLegacyModulesConfig).toBe(false);
311
- expect(Object.keys(infra).sort()).toEqual(['modules', 'modulesConfig']);
314
+ expect(Object.keys(infra).sort()).toEqual(['environments', 'modules', 'modulesConfig']);
312
315
  expect(expoConfig.plugins).toEqual(['expo-router']);
313
316
  });
314
317
 
@@ -367,10 +370,9 @@ describe('contracts', () => {
367
370
  unauthorizedRoute: '/sign-in',
368
371
  };
369
372
 
370
- const auth: AuthSpec = {
373
+ const auth: InfraAuthSpec = {
371
374
  scope: 'global',
372
375
  provider: 'supabase',
373
- authorization: { kind: 'RBAC', engine: 'cerbos' },
374
376
  flow: authFlow,
375
377
  signIn: { identifiers: ['email'] },
376
378
  signUp: {
@@ -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 { APP_DEPLOY_ENVIRONMENT_IDS, APP_DEPLOY_TARGET_IDS, isAppDeployManifest } from './deploy';
8
- import { DEPLOYMENT_TARGETS } from './types';
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: { deployment: { target: 'minikube', monitoring: true }, modules: [] },
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(DEPLOYMENT_TARGETS).toEqual(['minikube']);
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(APP_DEPLOY_ENVIRONMENT_IDS).toEqual(['local', 'preview', 'production']);
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', () => {