@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.
Files changed (256) hide show
  1. package/CHANGELOG.md +37 -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/splashScreen.d.ts +15 -0
  62. package/dist/appManifest/splashScreen.d.ts.map +1 -0
  63. package/dist/appManifest/splashScreen.js +2 -0
  64. package/dist/appManifest/splashScreen.js.map +1 -0
  65. package/dist/appManifest.d.ts +4 -18
  66. package/dist/appManifest.d.ts.map +1 -1
  67. package/dist/appManifest.js +2 -80
  68. package/dist/appManifest.js.map +1 -1
  69. package/dist/auth.d.ts +3 -2
  70. package/dist/auth.d.ts.map +1 -1
  71. package/dist/auth.js.map +1 -1
  72. package/dist/deploy.d.ts +0 -9
  73. package/dist/deploy.d.ts.map +1 -1
  74. package/dist/deploy.js +0 -8
  75. package/dist/deploy.js.map +1 -1
  76. package/dist/environments.d.ts +4 -0
  77. package/dist/environments.d.ts.map +1 -0
  78. package/dist/environments.js +3 -0
  79. package/dist/environments.js.map +1 -0
  80. package/dist/index.d.ts +2 -1
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +2 -1
  83. package/dist/index.js.map +1 -1
  84. package/dist/infra/constants.d.ts +91 -0
  85. package/dist/infra/constants.d.ts.map +1 -0
  86. package/dist/infra/constants.js +91 -0
  87. package/dist/infra/constants.js.map +1 -0
  88. package/dist/infra/infraFields.d.ts +12 -0
  89. package/dist/infra/infraFields.d.ts.map +1 -0
  90. package/dist/infra/infraFields.js +14 -0
  91. package/dist/infra/infraFields.js.map +1 -0
  92. package/dist/infra/isInfraAdapterDescriptor.d.ts +4 -0
  93. package/dist/infra/isInfraAdapterDescriptor.d.ts.map +1 -0
  94. package/dist/infra/isInfraAdapterDescriptor.js +31 -0
  95. package/dist/infra/isInfraAdapterDescriptor.js.map +1 -0
  96. package/dist/infra/isInfraAuthSpec.d.ts +4 -0
  97. package/dist/infra/isInfraAuthSpec.d.ts.map +1 -0
  98. package/dist/infra/isInfraAuthSpec.js +75 -0
  99. package/dist/infra/isInfraAuthSpec.js.map +1 -0
  100. package/dist/infra/isInfraCredentialRef.d.ts +3 -0
  101. package/dist/infra/isInfraCredentialRef.d.ts.map +1 -0
  102. package/dist/infra/isInfraCredentialRef.js +10 -0
  103. package/dist/infra/isInfraCredentialRef.js.map +1 -0
  104. package/dist/infra/isInfraDeploymentSpec.d.ts +4 -0
  105. package/dist/infra/isInfraDeploymentSpec.d.ts.map +1 -0
  106. package/dist/infra/isInfraDeploymentSpec.js +66 -0
  107. package/dist/infra/isInfraDeploymentSpec.js.map +1 -0
  108. package/dist/infra/isInfraEnvironmentSpec.d.ts +4 -0
  109. package/dist/infra/isInfraEnvironmentSpec.d.ts.map +1 -0
  110. package/dist/infra/isInfraEnvironmentSpec.js +82 -0
  111. package/dist/infra/isInfraEnvironmentSpec.js.map +1 -0
  112. package/dist/infra/isInfraManifest.d.ts +4 -0
  113. package/dist/infra/isInfraManifest.d.ts.map +1 -0
  114. package/dist/infra/isInfraManifest.js +23 -0
  115. package/dist/infra/isInfraManifest.js.map +1 -0
  116. package/dist/infra/isInfraShape.d.ts +3 -0
  117. package/dist/infra/isInfraShape.d.ts.map +1 -0
  118. package/dist/infra/isInfraShape.js +10 -0
  119. package/dist/infra/isInfraShape.js.map +1 -0
  120. package/dist/infra/isInfraWorkloadHealth.d.ts +3 -0
  121. package/dist/infra/isInfraWorkloadHealth.d.ts.map +1 -0
  122. package/dist/infra/isInfraWorkloadHealth.js +37 -0
  123. package/dist/infra/isInfraWorkloadHealth.js.map +1 -0
  124. package/dist/infra/isInfraWorkloadSpec.d.ts +4 -0
  125. package/dist/infra/isInfraWorkloadSpec.d.ts.map +1 -0
  126. package/dist/infra/isInfraWorkloadSpec.js +70 -0
  127. package/dist/infra/isInfraWorkloadSpec.js.map +1 -0
  128. package/dist/infra/isInfraWorkloadValue.d.ts +3 -0
  129. package/dist/infra/isInfraWorkloadValue.d.ts.map +1 -0
  130. package/dist/infra/isInfraWorkloadValue.js +40 -0
  131. package/dist/infra/isInfraWorkloadValue.js.map +1 -0
  132. package/dist/infra/parseInfraManifest.d.ts +5 -0
  133. package/dist/infra/parseInfraManifest.d.ts.map +1 -0
  134. package/dist/infra/parseInfraManifest.js +17 -0
  135. package/dist/infra/parseInfraManifest.js.map +1 -0
  136. package/dist/infra/validateInfraAdapterSelection.d.ts +6 -0
  137. package/dist/infra/validateInfraAdapterSelection.d.ts.map +1 -0
  138. package/dist/infra/validateInfraAdapterSelection.js +36 -0
  139. package/dist/infra/validateInfraAdapterSelection.js.map +1 -0
  140. package/dist/infra.d.ts +16 -0
  141. package/dist/infra.d.ts.map +1 -0
  142. package/dist/infra.js +11 -0
  143. package/dist/infra.js.map +1 -0
  144. package/dist/media.d.ts +7 -6
  145. package/dist/media.d.ts.map +1 -1
  146. package/dist/media.js +5 -5
  147. package/dist/media.js.map +1 -1
  148. package/dist/state.d.ts +5 -0
  149. package/dist/state.d.ts.map +1 -1
  150. package/dist/state.js.map +1 -1
  151. package/dist/types/appManifest.d.ts +9 -0
  152. package/dist/types/appManifest.d.ts.map +1 -0
  153. package/dist/types/appManifest.js +2 -0
  154. package/dist/types/appManifest.js.map +1 -0
  155. package/dist/types/infraAdapters.d.ts +85 -0
  156. package/dist/types/infraAdapters.d.ts.map +1 -0
  157. package/dist/types/infraAdapters.js +2 -0
  158. package/dist/types/infraAdapters.js.map +1 -0
  159. package/dist/types/infraLifecycle.d.ts +107 -0
  160. package/dist/types/infraLifecycle.d.ts.map +1 -0
  161. package/dist/types/infraLifecycle.js +2 -0
  162. package/dist/types/infraLifecycle.js.map +1 -0
  163. package/dist/types/infraManifest.d.ts +142 -0
  164. package/dist/types/infraManifest.d.ts.map +1 -0
  165. package/dist/types/infraManifest.js +2 -0
  166. package/dist/types/infraManifest.js.map +1 -0
  167. package/dist/types/infraSecrets.d.ts +15 -0
  168. package/dist/types/infraSecrets.d.ts.map +1 -0
  169. package/dist/types/infraSecrets.js +2 -0
  170. package/dist/types/infraSecrets.js.map +1 -0
  171. package/dist/types/infraValidation.d.ts +5 -0
  172. package/dist/types/infraValidation.d.ts.map +1 -0
  173. package/dist/types/infraValidation.js +2 -0
  174. package/dist/types/infraValidation.js.map +1 -0
  175. package/dist/types/infraWorkload.d.ts +70 -0
  176. package/dist/types/infraWorkload.d.ts.map +1 -0
  177. package/dist/types/infraWorkload.js +2 -0
  178. package/dist/types/infraWorkload.js.map +1 -0
  179. package/dist/types.d.ts +8 -82
  180. package/dist/types.d.ts.map +1 -1
  181. package/dist/types.js +0 -9
  182. package/dist/types.js.map +1 -1
  183. package/package.json +13 -3
  184. package/src/appManifest/apis.ts +18 -16
  185. package/src/appManifest/bindings.ts +18 -1
  186. package/src/appManifest/constants.ts +19 -0
  187. package/src/appManifest/data.ts +23 -9
  188. package/src/appManifest/dataSources.ts +8 -7
  189. package/src/appManifest/deploy.ts +17 -15
  190. package/src/appManifest/icon.ts +3 -1
  191. package/src/appManifest/isAppManifest.ts +95 -0
  192. package/src/appManifest/isAppStateSpec.ts +13 -0
  193. package/src/appManifest/isManifestValue.ts +19 -0
  194. package/src/appManifest/media.ts +11 -10
  195. package/src/appManifest/navigator.ts +7 -6
  196. package/src/appManifest/navigatorOptions.ts +9 -16
  197. package/src/appManifest/parseAppManifest.ts +15 -0
  198. package/src/appManifest/screens.ts +14 -2
  199. package/src/appManifest/splashScreen.test.ts +40 -0
  200. package/src/appManifest/splashScreen.ts +24 -0
  201. package/src/appManifest.test.ts +9 -5
  202. package/src/appManifest.ts +8 -106
  203. package/src/auth-flow-contract.test.ts +6 -10
  204. package/src/auth-oauth.test.ts +2 -2
  205. package/src/auth.ts +3 -2
  206. package/src/bindings.test.ts +5 -0
  207. package/src/contracts.test.ts +21 -39
  208. package/src/deploy.test.ts +13 -5
  209. package/src/deploy.ts +0 -11
  210. package/src/environments.ts +4 -0
  211. package/src/index.ts +2 -1
  212. package/src/infra/constants.ts +91 -0
  213. package/src/infra/infraFields.ts +20 -0
  214. package/src/infra/isInfraAdapterDescriptor.ts +37 -0
  215. package/src/infra/isInfraAuthSpec.ts +98 -0
  216. package/src/infra/isInfraCredentialRef.ts +11 -0
  217. package/src/infra/isInfraDeploymentSpec.ts +75 -0
  218. package/src/infra/isInfraEnvironmentSpec.ts +95 -0
  219. package/src/infra/isInfraManifest.ts +29 -0
  220. package/src/infra/isInfraShape.ts +16 -0
  221. package/src/infra/isInfraWorkloadHealth.ts +38 -0
  222. package/src/infra/isInfraWorkloadSpec.ts +87 -0
  223. package/src/infra/isInfraWorkloadValue.ts +41 -0
  224. package/src/infra/parseInfraManifest.ts +20 -0
  225. package/src/infra/validateInfraAdapterSelection.ts +42 -0
  226. package/src/infra-types.test.ts +78 -0
  227. package/src/infra-workload.test.ts +186 -0
  228. package/src/infra.test.ts +268 -0
  229. package/src/infra.ts +15 -0
  230. package/src/media.ts +12 -11
  231. package/src/profile-contract.test.ts +2 -3
  232. package/src/secrets.test.ts +19 -13
  233. package/src/state.ts +6 -0
  234. package/src/types/appManifest.ts +5 -0
  235. package/src/types/infraAdapters.ts +148 -0
  236. package/src/types/infraLifecycle.ts +105 -0
  237. package/src/types/infraManifest.ts +130 -0
  238. package/src/types/infraSecrets.ts +16 -0
  239. package/src/types/infraValidation.ts +2 -0
  240. package/src/types/infraWorkload.ts +64 -0
  241. package/src/types.ts +8 -103
  242. package/dist/appManifest/infra.d.ts +0 -2
  243. package/dist/appManifest/infra.d.ts.map +0 -1
  244. package/dist/appManifest/infra.js +0 -130
  245. package/dist/appManifest/infra.js.map +0 -1
  246. package/dist/appManifest/shared.d.ts +0 -8
  247. package/dist/appManifest/shared.d.ts.map +0 -1
  248. package/dist/appManifest/shared.js +0 -31
  249. package/dist/appManifest/shared.js.map +0 -1
  250. package/dist/secretManifest.d.ts +0 -11
  251. package/dist/secretManifest.d.ts.map +0 -1
  252. package/dist/secretManifest.js +0 -2
  253. package/dist/secretManifest.js.map +0 -1
  254. package/src/appManifest/infra.ts +0 -190
  255. package/src/appManifest/shared.ts +0 -40
  256. 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) &&
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import type { AppManifest, SplashScreenSpec } from '../index';
4
+
5
+ describe('AppManifest splash screen contract', () => {
6
+ it('accepts portable media references with Expo-owned option types', () => {
7
+ const splashScreen: SplashScreenSpec = {
8
+ backgroundColor: '#ffffff',
9
+ image: { mediaId: 'splash-logo' },
10
+ imageWidth: 160,
11
+ resizeMode: 'contain',
12
+ dark: {
13
+ backgroundColor: '#000000',
14
+ image: { mediaId: 'splash-logo-dark' },
15
+ },
16
+ };
17
+
18
+ const manifest: Pick<AppManifest, 'splashScreen'> = { splashScreen };
19
+
20
+ expect(JSON.parse(JSON.stringify(manifest))).toEqual({ splashScreen });
21
+ });
22
+
23
+ it('keeps root-only Expo options out of dark-mode overrides', () => {
24
+ const splashWithDarkImageWidth: SplashScreenSpec = {
25
+ dark: {
26
+ // @ts-expect-error Expo dark-mode options do not include image sizing.
27
+ imageWidth: 160,
28
+ },
29
+ };
30
+ const splashWithDarkResizeMode: SplashScreenSpec = {
31
+ dark: {
32
+ // @ts-expect-error Expo dark-mode options do not include resize mode.
33
+ resizeMode: 'contain',
34
+ },
35
+ };
36
+
37
+ expect(splashWithDarkImageWidth.dark).toEqual({ imageWidth: 160 });
38
+ expect(splashWithDarkResizeMode.dark).toEqual({ resizeMode: 'contain' });
39
+ });
40
+ });
@@ -0,0 +1,24 @@
1
+ import type { Props as ExpoSplashScreenPluginProps } from 'expo-splash-screen/plugin';
2
+
3
+ import type { MediaAssetReference } from '../media';
4
+
5
+ type ExpoSplashScreenRootProps = Pick<
6
+ ExpoSplashScreenPluginProps,
7
+ 'backgroundColor' | 'image' | 'imageWidth' | 'resizeMode'
8
+ >;
9
+
10
+ type PortableSplashScreenProps<T extends { readonly image?: string }> = Readonly<
11
+ Omit<T, 'image'>
12
+ > & {
13
+ readonly image?: MediaAssetReference;
14
+ };
15
+
16
+ export type SplashScreenResizeMode = NonNullable<ExpoSplashScreenPluginProps['resizeMode']>;
17
+
18
+ export type SplashScreenModeSpec = PortableSplashScreenProps<
19
+ NonNullable<ExpoSplashScreenPluginProps['dark']>
20
+ >;
21
+
22
+ export type SplashScreenSpec = PortableSplashScreenProps<ExpoSplashScreenRootProps> & {
23
+ readonly dark?: SplashScreenModeSpec;
24
+ };
@@ -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,106 +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
-
16
- export type AppManifestParseResult =
17
- | { readonly ok: true; readonly manifest: AppManifest }
18
- | { readonly ok: false; readonly message: string };
19
-
20
- const APP_MANIFEST_KEY_POLICY = {
21
- metadata: 'required',
22
- themes: 'required',
23
- activeThemeId: 'required',
24
- activeThemeMode: 'optional',
25
- splashScreen: 'optional',
26
- media: 'optional',
27
- deploy: 'optional',
28
- infra: 'required',
29
- navigator: 'required',
30
- screens: 'required',
31
- dataSources: 'optional',
32
- dataBindings: 'optional',
33
- repository: 'optional',
34
- settings: 'required',
35
- } as const satisfies Record<keyof AppManifest, 'optional' | 'required'>;
36
-
37
- /**
38
- * Parse unknown JSON-compatible input at the canonical AppManifest boundary.
39
- *
40
- * Contracts owns structural manifest validation. Consumers may add semantic
41
- * diagnostics after this parser succeeds, but should not reconstruct the
42
- * AppManifest shape in their own packages.
43
- */
44
- export function parseAppManifest(value: unknown): AppManifestParseResult {
45
- return isAppManifest(value)
46
- ? { ok: true, manifest: value }
47
- : { ok: false, message: 'Value is not a canonical AppManifest.' };
48
- }
49
-
50
- /** Return whether an unknown value satisfies the canonical AppManifest shape. */
51
- export function isAppManifest(value: unknown): value is AppManifest {
52
- return (
53
- isRecord(value) &&
54
- hasRequiredManifestKeys(value) &&
55
- !('generatedApis' in value) &&
56
- isManifestMetadata(value.metadata) &&
57
- Array.isArray(value.themes) &&
58
- value.themes.every(isThemeConfig) &&
59
- typeof value.activeThemeId === 'string' &&
60
- isActiveThemeMode(value.activeThemeMode) &&
61
- (value.splashScreen === undefined || isSplashScreenSpec(value.splashScreen)) &&
62
- (value.media === undefined || isMediaManifest(value.media)) &&
63
- (value.deploy === undefined || isAppDeployManifest(value.deploy)) &&
64
- isInfraManifest(value.infra) &&
65
- isAppNavigatorManifest(value.navigator) &&
66
- isScreenRegistry(value.screens) &&
67
- (value.dataSources === undefined || isDataSourceRegistry(value.dataSources)) &&
68
- (value.dataBindings === undefined || isComponentDataBindingRegistry(value.dataBindings)) &&
69
- (value.repository === undefined || isRepositoryManifest(value.repository)) &&
70
- isAppSettings(value.settings)
71
- );
72
- }
73
-
74
- function hasRequiredManifestKeys(value: Record<string, unknown>): boolean {
75
- return Object.entries(APP_MANIFEST_KEY_POLICY).every(
76
- ([key, policy]) => policy === 'optional' || key in value,
77
- );
78
- }
79
-
80
- function isActiveThemeMode(value: unknown): boolean {
81
- return value === undefined || value === 'dark' || value === 'light';
82
- }
83
-
84
- function isRepositoryManifest(value: unknown): boolean {
85
- return (
86
- isRecord(value) &&
87
- value.provider === 'github' &&
88
- typeof value.owner === 'string' &&
89
- value.owner.length > 0 &&
90
- typeof value.name === 'string' &&
91
- value.name.length > 0 &&
92
- typeof value.url === 'string' &&
93
- value.url.length > 0 &&
94
- value.defaultBranch === 'main'
95
- );
96
- }
97
-
98
- function isAppSettings(value: unknown): boolean {
99
- return (
100
- isRecord(value) &&
101
- !('apiBaseUrl' in value) &&
102
- isRecord(value.localization) &&
103
- typeof value.localization.defaultLocale === 'string' &&
104
- isStringArray(value.localization.locales)
105
- );
106
- }
1
+ export { isAppManifest } from './appManifest/isAppManifest';
2
+ export { parseAppManifest } from './appManifest/parseAppManifest';
3
+ export type {
4
+ SplashScreenModeSpec,
5
+ SplashScreenResizeMode,
6
+ SplashScreenSpec,
7
+ } from './appManifest/splashScreen';
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',