@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
@@ -0,0 +1,268 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import {
4
+ INFRA_ADAPTER_CATALOG,
5
+ INFRA_RUNTIME_COMPATIBILITY,
6
+ type InfraEnvironmentSpec,
7
+ type InfraManifest,
8
+ isInfraAdapterDescriptor,
9
+ isInfraDeploymentSpec,
10
+ isInfraEnvironmentSpec,
11
+ isInfraManifest,
12
+ parseInfraManifest,
13
+ validateInfraAdapterSelection,
14
+ } from './infra';
15
+
16
+ const local = {
17
+ deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
18
+ } as const satisfies InfraEnvironmentSpec;
19
+ const production = {
20
+ deployment: {
21
+ compute: { provider: 'hetzner', location: 'nbg1' },
22
+ runtime: { provider: 'k3s', topology: { servers: 1, agents: 0 } },
23
+ },
24
+ database: { provider: 'supabase' },
25
+ auth: { provider: 'supabase' },
26
+ objectStorage: { provider: 'r2', buckets: ['media'] },
27
+ authz: { provider: 'cerbos', kind: 'ABAC' },
28
+ secretStore: { provider: 'supabase-vault' },
29
+ networking: { domain: 'api.example.ch' },
30
+ } as const satisfies InfraEnvironmentSpec;
31
+
32
+ describe('standalone Infra environments', () => {
33
+ it('parses local/preview/production without an AppManifest or Deploy', () => {
34
+ const manifest = {
35
+ environments: { local, preview: production, production },
36
+ modules: [],
37
+ } satisfies InfraManifest;
38
+ expect(parseInfraManifest(JSON.parse(JSON.stringify(manifest)))).toEqual({
39
+ ok: true,
40
+ value: manifest,
41
+ diagnostics: [],
42
+ });
43
+ });
44
+
45
+ it.each([
46
+ null,
47
+ {},
48
+ { modules: [] },
49
+ { environments: {}, modules: [] },
50
+ { environments: { production }, modules: [] },
51
+ { environments: { local, staging: local }, modules: [] },
52
+ { environments: { local, preview: null }, modules: [] },
53
+ { environments: { local }, modules: [42] },
54
+ { environments: { local }, modules: [], modulesConfig: [] },
55
+ { environments: { local }, modules: [], apis: 'invalid' },
56
+ ])('rejects invalid environment boundaries: %j', (value) => {
57
+ expect(isInfraManifest(value)).toBe(false);
58
+ expect(parseInfraManifest(value).ok).toBe(false);
59
+ });
60
+
61
+ it.each([
62
+ 'deployment',
63
+ 'storage',
64
+ 'state',
65
+ 'auth',
66
+ 'database',
67
+ 'networking',
68
+ 'secretStore',
69
+ 'plugins',
70
+ 'pluginsConfig',
71
+ ])('rejects removed top-level %s', (key) => {
72
+ expect(isInfraManifest({ environments: { local }, modules: [], [key]: {} })).toBe(false);
73
+ });
74
+
75
+ it.each(['storage', 'state', 'monitoring', 'observability', 'target'])(
76
+ 'rejects obsolete or speculative environment %s',
77
+ (key) => {
78
+ expect(isInfraEnvironmentSpec({ ...local, [key]: {} })).toBe(false);
79
+ },
80
+ );
81
+ });
82
+
83
+ describe('exact deployment compatibility', () => {
84
+ it('tests every registered compute/runtime pair against the same catalog', () => {
85
+ const computes = Object.values(INFRA_ADAPTER_CATALOG).filter(
86
+ (entry) => entry.kind === 'compute',
87
+ );
88
+ for (const compute of computes) {
89
+ for (const [runtime, allowed] of Object.entries(INFRA_RUNTIME_COMPATIBILITY)) {
90
+ const selection = {
91
+ compute: {
92
+ provider: compute.id,
93
+ ...(compute.id === 'hetzner' ? { location: 'nbg1' } : {}),
94
+ },
95
+ runtime: { provider: runtime },
96
+ };
97
+ expect(isInfraDeploymentSpec(selection)).toBe(allowed.some((id) => id === compute.id));
98
+ }
99
+ }
100
+ });
101
+
102
+ it.each([
103
+ { compute: { provider: 'gcp' }, runtime: { provider: 'ecs' } },
104
+ { compute: { provider: 'hetzner', location: 'nbg1' }, runtime: { provider: 'eks' } },
105
+ { compute: { provider: 'hetzner', location: 'nbg1' }, runtime: { provider: 'minikube' } },
106
+ { compute: { provider: 'hetzner' }, runtime: { provider: 'k3s' } },
107
+ { compute: { provider: 'hetzner', location: ' ' }, runtime: { provider: 'k3s' } },
108
+ { compute: { provider: 'local', location: 'nbg1' }, runtime: { provider: 'k3s' } },
109
+ { ...local.deployment, monitoring: false },
110
+ { target: 'minikube', monitoring: true },
111
+ ])('rejects unsupported/config-mismatched deployment: %j', (value) => {
112
+ expect(isInfraDeploymentSpec(value)).toBe(false);
113
+ });
114
+ });
115
+
116
+ describe('runtime topology configuration', () => {
117
+ it.each([
118
+ { provider: 'k3s', topology: { servers: 0, agents: 1 } },
119
+ { provider: 'k3s', topology: { servers: 1, agents: -1 } },
120
+ { provider: 'k3s', topology: { servers: 1.5, agents: 0 } },
121
+ { provider: 'k3s', topology: { servers: 1, agents: Infinity } },
122
+ { provider: 'k3s', topology: { servers: 1 } },
123
+ { provider: 'minikube', cpus: 0 },
124
+ { provider: 'minikube', memoryMiB: NaN },
125
+ { provider: 'minikube', driver: 'unknown' },
126
+ { provider: 'docker-compose', profile: 'minikube' },
127
+ ])('rejects malformed runtime configuration: %j', (runtime) => {
128
+ expect(isInfraDeploymentSpec({ compute: { provider: 'local' }, runtime })).toBe(false);
129
+ });
130
+
131
+ it('accepts multi-node topology on both supported k3s computes', () => {
132
+ for (const compute of [{ provider: 'local' }, { provider: 'hetzner', location: 'nbg1' }]) {
133
+ expect(
134
+ isInfraDeploymentSpec({
135
+ compute,
136
+ runtime: { provider: 'k3s', topology: { servers: 3, agents: 4 } },
137
+ }),
138
+ ).toBe(true);
139
+ }
140
+ });
141
+ });
142
+
143
+ describe('sibling service configuration', () => {
144
+ it('accepts Supabase auth, Cerbos and independent R2 storage', () => {
145
+ expect(isInfraEnvironmentSpec(production)).toBe(true);
146
+ expect(isInfraEnvironmentSpec({ ...production, objectStorage: { provider: 'supabase' } })).toBe(
147
+ true,
148
+ );
149
+ expect(isInfraEnvironmentSpec({ ...local, auth: { provider: 'supabase' } })).toBe(true);
150
+ });
151
+
152
+ it.each([
153
+ { database: { provider: 'postgres' } },
154
+ { database: { provider: 'supabase', tier: 'enterprise' } },
155
+ { objectStorage: { provider: 'auto' } },
156
+ { objectStorage: { provider: 's3' } },
157
+ { objectStorage: { provider: 'supabase', buckets: [1] } },
158
+ { objectStorage: { provider: 'supabase', accountId: 'r2-account' } },
159
+ { auth: { provider: 'custom' } },
160
+ { auth: { provider: 'supabase', scope: 'unknown' } },
161
+ { auth: { provider: 'supabase', authorization: { kind: 'ABAC', engine: 'cerbos' } } },
162
+ { authz: { provider: 'native', kind: 'RBAC' } },
163
+ { authz: { engine: 'cerbos', kind: 'ABAC' } },
164
+ { authz: { provider: 'cerbos', kind: 'unknown' } },
165
+ { secretStore: { provider: 'unknown' } },
166
+ { secretStore: { provider: 'supabase-vault' } },
167
+ { networking: { domain: 'example.ch', cdn: true } },
168
+ ])('rejects unsupported service selection: %j', (selection) => {
169
+ expect(isInfraEnvironmentSpec({ ...local, ...selection })).toBe(false);
170
+ });
171
+ });
172
+
173
+ describe('authentication configuration', () => {
174
+ it('preserves auth flow, profile and OAuth credential references', () => {
175
+ expect(
176
+ isInfraEnvironmentSpec({
177
+ ...production,
178
+ auth: {
179
+ provider: 'supabase',
180
+ scope: 'global',
181
+ flow: { signInRoute: '/sign-in', postSignInRoute: '/' },
182
+ signIn: { identifiers: ['email'] },
183
+ signUp: { requiredFields: ['email', 'password'], signUpPolicy: 'requireVerification' },
184
+ profile: {
185
+ fields: ['email'],
186
+ primaryKey: 'authUserId',
187
+ createStrategy: 'trigger',
188
+ updateStrategy: 'api',
189
+ },
190
+ oauth: {
191
+ enabled: true,
192
+ callbackRoute: '/auth/callback',
193
+ providers: [{ id: 'google', credentialsRef: 'auth/oauth/google' }],
194
+ },
195
+ },
196
+ }),
197
+ ).toBe(true);
198
+ });
199
+
200
+ it.each([
201
+ { flow: { signInRoute: '/' } },
202
+ { signIn: { identifiers: ['social'] } },
203
+ { signUp: { requiredFields: [], signUpPolicy: 'unknown' } },
204
+ { profile: { fields: ['email'], createStrategy: 'unknown' } },
205
+ {
206
+ oauth: {
207
+ enabled: true,
208
+ callbackRoute: '/',
209
+ providers: [{ id: 'google', clientSecret: 'sentinel' }],
210
+ },
211
+ },
212
+ ])('rejects malformed auth configuration: %j', (config) => {
213
+ expect(isInfraEnvironmentSpec({ ...local, auth: { provider: 'supabase', ...config } })).toBe(
214
+ false,
215
+ );
216
+ });
217
+ });
218
+
219
+ describe('adapter package discovery contracts', () => {
220
+ it('accepts every canonical descriptor and deduplicates a multi-capability Supabase platform', () => {
221
+ for (const descriptor of Object.values(INFRA_ADAPTER_CATALOG))
222
+ expect(isInfraAdapterDescriptor(descriptor)).toBe(true);
223
+ const environment = { ...production, objectStorage: { provider: 'supabase' } } as const;
224
+ const result = validateInfraAdapterSelection(environment, Object.values(INFRA_ADAPTER_CATALOG));
225
+ expect(result.ok).toBe(true);
226
+ if (!result.ok) throw new Error('Expected installed descriptors.');
227
+ expect(result.value.filter((descriptor) => descriptor.id === 'supabase')).toHaveLength(1);
228
+ expect(result.value.some((descriptor) => descriptor.id === 'r2')).toBe(false);
229
+ });
230
+
231
+ it.each([
232
+ { package: '@ankhorage/supabase-db' },
233
+ { id: 'custom' },
234
+ { kind: 'runtime' },
235
+ { configVersion: 2 },
236
+ { capabilities: ['auth'] },
237
+ { dependencies: [] },
238
+ { targets: ['local-host'] },
239
+ { operations: ['drop-all'] },
240
+ ])('rejects a mismatched package descriptor: %j', (override) => {
241
+ expect(isInfraAdapterDescriptor({ ...INFRA_ADAPTER_CATALOG.supabase, ...override })).toBe(
242
+ false,
243
+ );
244
+ });
245
+
246
+ it('reports the exact missing selected package without requiring unselected providers', () => {
247
+ expect(
248
+ validateInfraAdapterSelection(local, [
249
+ INFRA_ADAPTER_CATALOG.local,
250
+ INFRA_ADAPTER_CATALOG.minikube,
251
+ ]).ok,
252
+ ).toBe(true);
253
+ const result = validateInfraAdapterSelection(local, [INFRA_ADAPTER_CATALOG.local]);
254
+ expect(result.ok).toBe(false);
255
+ expect(result.diagnostics[0]?.message).toContain('@ankhorage/minikube');
256
+ expect(result.diagnostics[0]?.code).toBe('adapter_unavailable');
257
+ });
258
+
259
+ it('rejects ambiguous duplicate installed descriptors', () => {
260
+ expect(
261
+ validateInfraAdapterSelection(local, [
262
+ INFRA_ADAPTER_CATALOG.local,
263
+ INFRA_ADAPTER_CATALOG.local,
264
+ INFRA_ADAPTER_CATALOG.minikube,
265
+ ]).ok,
266
+ ).toBe(false);
267
+ });
268
+ });
package/src/infra.ts ADDED
@@ -0,0 +1,15 @@
1
+ /** Canonical standalone infrastructure contracts, catalog and side-effect-free validation. */
2
+ export { INFRA_ADAPTER_CATALOG, INFRA_RUNTIME_COMPATIBILITY } from './infra/constants';
3
+ export { isInfraAdapterDescriptor } from './infra/isInfraAdapterDescriptor';
4
+ export { isInfraAuthSpec } from './infra/isInfraAuthSpec';
5
+ export { isInfraDeploymentSpec } from './infra/isInfraDeploymentSpec';
6
+ export { isInfraEnvironmentSpec } from './infra/isInfraEnvironmentSpec';
7
+ export { isInfraManifest } from './infra/isInfraManifest';
8
+ export { isInfraWorkloadSpec } from './infra/isInfraWorkloadSpec';
9
+ export { parseInfraManifest } from './infra/parseInfraManifest';
10
+ export { validateInfraAdapterSelection } from './infra/validateInfraAdapterSelection';
11
+ export type * from './types/infraAdapters';
12
+ export type * from './types/infraLifecycle';
13
+ export type * from './types/infraManifest';
14
+ export type * from './types/infraSecrets';
15
+ export type * from './types/infraWorkload';
package/src/media.ts CHANGED
@@ -1,10 +1,13 @@
1
+ import { isRecord } from '@ankhorage/utility/object';
2
+ import { isNonEmptyString } from '@ankhorage/utility/string';
3
+
1
4
  export const MEDIA_ASSET_KINDS = ['image', 'audio', 'video', 'font', 'file'] as const;
2
5
 
3
6
  export type MediaAssetKind = (typeof MEDIA_ASSET_KINDS)[number];
4
7
 
5
8
  export interface MediaStorageSource {
6
9
  readonly kind: 'storage';
7
- /** Optional logical storage connection identifier for future multi-storage apps. */
10
+ /*** Optional logical storage connection identifier for future multi-storage apps. */
8
11
  readonly storageId?: string;
9
12
  readonly bucket: string;
10
13
  readonly path: string;
@@ -12,13 +15,13 @@ export interface MediaStorageSource {
12
15
 
13
16
  export interface MediaUrlSource {
14
17
  readonly kind: 'url';
15
- /** Stable remote URL. Transient/local URL schemes are not canonical media sources. */
18
+ /*** Stable remote URL. Transient/local URL schemes are not canonical media sources. */
16
19
  readonly url: string;
17
20
  }
18
21
 
19
22
  export interface MediaBundledSource {
20
23
  readonly kind: 'bundled';
21
- /** App-relative bundled asset path resolved by the generated/runtime host. */
24
+ /*** App-relative bundled asset path resolved by the generated/runtime host. */
22
25
  readonly path: string;
23
26
  }
24
27
 
@@ -33,7 +36,7 @@ export interface MediaAssetMetadata {
33
36
  readonly durationMs?: number;
34
37
  }
35
38
 
36
- /** Canonical Studio-managed authoring media entry. */
39
+ /*** Canonical Studio-managed authoring media entry. */
37
40
  export interface MediaAsset {
38
41
  readonly id: string;
39
42
  readonly name: string;
@@ -45,24 +48,22 @@ export interface MediaAsset {
45
48
 
46
49
  export type MediaAssetRegistry = Readonly<Record<string, MediaAsset>>;
47
50
 
48
- /** App-authoring media pool. Runtime/user-generated uploads do not belong here. */
51
+ /*** App-authoring media pool. Runtime/user-generated uploads do not belong here. */
49
52
  export interface MediaManifest {
50
53
  readonly assets: MediaAssetRegistry;
51
54
  }
52
55
 
53
- /** Stable component/property reference to one entry in `AppManifest.media.assets`. */
56
+ /*** Stable component/property reference to one entry in `AppManifest.media.assets`. */
54
57
  export interface MediaAssetReference {
55
58
  readonly mediaId: string;
56
59
  }
57
60
 
61
+ /*** Validate an authored media reference with exactly one enumerable key and a non-empty media ID. */
58
62
  export function isMediaAssetReference(value: unknown): value is MediaAssetReference {
59
63
  return (
60
- typeof value === 'object' &&
61
- value !== null &&
62
- !Array.isArray(value) &&
64
+ isRecord(value) &&
63
65
  Object.keys(value).length === 1 &&
64
66
  'mediaId' in value &&
65
- typeof value.mediaId === 'string' &&
66
- value.mediaId.trim().length > 0
67
+ isNonEmptyString(value.mediaId)
67
68
  );
68
69
  }
@@ -4,7 +4,7 @@ import {
4
4
  AUTH_PROFILE_CREATE_STRATEGIES,
5
5
  AUTH_PROFILE_PRIMARY_KEY_STRATEGIES,
6
6
  AUTH_PROFILE_UPDATE_STRATEGIES,
7
- type AuthSpec,
7
+ type InfraAuthSpec,
8
8
  } from './index';
9
9
 
10
10
  describe('profile contract', () => {
@@ -15,10 +15,9 @@ describe('profile contract', () => {
15
15
  });
16
16
 
17
17
  it('serializes profile settings on an auth spec', () => {
18
- const auth: AuthSpec = {
18
+ const auth: InfraAuthSpec = {
19
19
  scope: 'global',
20
20
  provider: 'supabase',
21
- authorization: { kind: 'RBAC', engine: 'native' },
22
21
  profile: {
23
22
  fields: ['email', 'displayName', 'avatarUrl'],
24
23
  table: 'profiles',
@@ -1,15 +1,13 @@
1
- import './secretManifest';
2
-
3
1
  import { describe, expect, test } from 'bun:test';
4
2
 
5
3
  import type { AuthOAuthProviderConfig } from './auth';
4
+ import type { InfraManifest } from './infra';
6
5
  import {
7
6
  findForbiddenInlineSecretFields,
8
7
  normalizeSecretRef,
9
8
  normalizeSecretScope,
10
9
  validateSecretPayload,
11
10
  } from './secrets';
12
- import type { InfraManifest } from './types';
13
11
 
14
12
  describe('secret-store contracts', () => {
15
13
  test('normalizes logical secret references', () => {
@@ -70,19 +68,27 @@ describe('secret-store contracts', () => {
70
68
 
71
69
  const infra: InfraManifest = {
72
70
  modules: [],
73
- secretStore: { provider: 'supabase-vault' },
74
- auth: {
75
- scope: 'global',
76
- provider: 'supabase',
77
- oauth: {
78
- enabled: true,
79
- callbackRoute: '/auth/callback',
80
- providers: [provider],
71
+ environments: {
72
+ local: {
73
+ deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
74
+ database: { provider: 'supabase' },
75
+ secretStore: { provider: 'supabase-vault' },
76
+ auth: {
77
+ scope: 'global',
78
+ provider: 'supabase',
79
+ oauth: {
80
+ enabled: true,
81
+ callbackRoute: '/auth/callback',
82
+ providers: [provider],
83
+ },
84
+ },
81
85
  },
82
86
  },
83
87
  };
84
88
 
85
- expect(infra.secretStore?.provider).toBe('supabase-vault');
86
- expect(infra.auth?.oauth?.providers[0]?.credentialsRef).toBe('auth/oauth/google');
89
+ expect(infra.environments.local.secretStore?.provider).toBe('supabase-vault');
90
+ expect(infra.environments.local.auth?.oauth?.providers[0]?.credentialsRef).toBe(
91
+ 'auth/oauth/google',
92
+ );
87
93
  });
88
94
  });
package/src/state.ts CHANGED
@@ -1,3 +1,9 @@
1
+ /** Legend currently implements in-memory state only; unsupported persistence cannot be selected. */
2
+ export interface AppStateSpec {
3
+ readonly provider: 'legend';
4
+ readonly persistence?: false;
5
+ }
6
+
1
7
  export type StatePrimitive = string | number | boolean | null;
2
8
 
3
9
  export type StateValue =
@@ -0,0 +1,5 @@
1
+ import type { AppManifest } from '../types';
2
+
3
+ export type AppManifestParseResult =
4
+ | { readonly ok: true; readonly manifest: AppManifest }
5
+ | { readonly ok: false; readonly message: string };
@@ -0,0 +1,148 @@
1
+ import type { AppEnvironmentId } from '../environments';
2
+ import type { INFRA_ADAPTER_CATALOG } from '../infra/constants';
3
+ import type {
4
+ InfraDestroyRequest,
5
+ InfraGeneratedArtifact,
6
+ InfraLedger,
7
+ InfraOutput,
8
+ InfraOwnedResource,
9
+ InfraPlanAction,
10
+ InfraResourceStatus,
11
+ InfraResult,
12
+ } from './infraLifecycle';
13
+ import type {
14
+ InfraAdapterId,
15
+ InfraComputeProviderId,
16
+ InfraComputeSelection,
17
+ InfraEnvironmentSpec,
18
+ InfraRuntimeProviderId,
19
+ InfraRuntimeSelection,
20
+ } from './infraManifest';
21
+ import type { InfraControlPlaneCredentialRef, InfraSecretReference } from './infraSecrets';
22
+ import type { InfraWorkloadSpec } from './infraWorkload';
23
+
24
+ /** Package exports must match this exact catalog entry; optional operations are advertised explicitly. */
25
+ export type InfraAdapterDescriptor<P extends InfraAdapterId = InfraAdapterId> = {
26
+ [K in P]: (typeof INFRA_ADAPTER_CATALOG)[K] & {
27
+ readonly operations?: readonly ('suspend' | 'resume' | 'generate' | 'diagnostics')[];
28
+ };
29
+ }[P];
30
+
31
+ export type InfraComputeTarget = {
32
+ readonly id: string;
33
+ readonly os: 'linux' | 'darwin' | 'windows';
34
+ readonly architecture: 'amd64' | 'arm64';
35
+ } & (
36
+ | { readonly kind: 'local-host' }
37
+ | {
38
+ readonly kind: 'ssh-host';
39
+ readonly host: string;
40
+ readonly port: number;
41
+ readonly user: string;
42
+ readonly credential: InfraControlPlaneCredentialRef;
43
+ /** Expected host key supplied by the compute owner; runtimes must verify host authenticity. */
44
+ readonly hostKeyFingerprint: string;
45
+ }
46
+ );
47
+
48
+ /** Trusted execution-only ports. Implementations must not serialize resolved credential values. */
49
+ export interface InfraExecutionContext {
50
+ readonly projectId: string;
51
+ readonly environment: AppEnvironmentId;
52
+ readonly desired: InfraEnvironmentSpec;
53
+ readonly previous?: InfraLedger;
54
+ readonly signal?: AbortSignal;
55
+ readonly credentials: {
56
+ resolveAsync(
57
+ reference: InfraControlPlaneCredentialRef,
58
+ ): Promise<InfraResult<Readonly<Record<string, string>>>>;
59
+ };
60
+ readonly secrets: {
61
+ resolveAsync(reference: InfraSecretReference): Promise<InfraResult<string>>;
62
+ };
63
+ }
64
+
65
+ export interface InfraReconcileResult {
66
+ readonly resources: readonly InfraOwnedResource[];
67
+ readonly outputs: readonly InfraOutput[];
68
+ }
69
+
70
+ export interface InfraComputeAdapter<P extends InfraComputeProviderId = InfraComputeProviderId> {
71
+ readonly descriptor: InfraAdapterDescriptor<P>;
72
+ /** Read-only prerequisites, config and control-plane credential validation. */
73
+ validateAsync(
74
+ context: InfraExecutionContext,
75
+ selection: InfraComputeSelection<P>,
76
+ ): Promise<InfraResult<null>>;
77
+ planAsync(
78
+ context: InfraExecutionContext,
79
+ selection: InfraComputeSelection<P>,
80
+ ): Promise<InfraResult<readonly InfraPlanAction[]>>;
81
+ ensureAsync(
82
+ context: InfraExecutionContext,
83
+ selection: InfraComputeSelection<P>,
84
+ ): Promise<
85
+ InfraResult<InfraReconcileResult & { readonly targets: readonly InfraComputeTarget[] }>
86
+ >;
87
+ statusAsync(context: InfraExecutionContext): Promise<InfraResult<readonly InfraResourceStatus[]>>;
88
+ destroyAsync(
89
+ context: InfraExecutionContext,
90
+ request: InfraDestroyRequest,
91
+ ): Promise<InfraResult<InfraReconcileResult>>;
92
+ }
93
+
94
+ export interface InfraRuntimeDesiredState<
95
+ P extends InfraRuntimeProviderId = InfraRuntimeProviderId,
96
+ > {
97
+ readonly selection: InfraRuntimeSelection<P>;
98
+ readonly targets: readonly InfraComputeTarget[];
99
+ readonly workloads: readonly InfraWorkloadSpec[];
100
+ }
101
+
102
+ export interface InfraRuntimeAdapter<P extends InfraRuntimeProviderId = InfraRuntimeProviderId> {
103
+ readonly descriptor: InfraAdapterDescriptor<P>;
104
+ /** Read-only prerequisites and desired-state validation; no cluster creation. */
105
+ validateAsync(
106
+ context: InfraExecutionContext,
107
+ desired: InfraRuntimeDesiredState<P>,
108
+ ): Promise<InfraResult<null>>;
109
+ planAsync(
110
+ context: InfraExecutionContext,
111
+ desired: InfraRuntimeDesiredState<P>,
112
+ ): Promise<InfraResult<readonly InfraPlanAction[]>>;
113
+ ensureAsync(
114
+ context: InfraExecutionContext,
115
+ desired: InfraRuntimeDesiredState<P>,
116
+ ): Promise<InfraResult<InfraReconcileResult>>;
117
+ statusAsync(context: InfraExecutionContext): Promise<InfraResult<readonly InfraResourceStatus[]>>;
118
+ suspendAsync(context: InfraExecutionContext): Promise<InfraResult<InfraReconcileResult>>;
119
+ destroyAsync(
120
+ context: InfraExecutionContext,
121
+ request: InfraDestroyRequest,
122
+ ): Promise<InfraResult<InfraReconcileResult>>;
123
+ generateAsync?(
124
+ context: InfraExecutionContext,
125
+ desired: InfraRuntimeDesiredState<P>,
126
+ ): Promise<InfraResult<readonly InfraGeneratedArtifact[]>>;
127
+ }
128
+
129
+ /** A service can own several capabilities but contributes one deduplicated platform instance. */
130
+ export interface InfraServiceAdapter {
131
+ readonly descriptor: InfraAdapterDescriptor<'supabase' | 'cerbos' | 'r2' | 'supabase-vault'>;
132
+ /** Read-only service config, bootstrap credentials and dependency validation. */
133
+ validateAsync(context: InfraExecutionContext): Promise<InfraResult<null>>;
134
+ planAsync(context: InfraExecutionContext): Promise<InfraResult<readonly InfraPlanAction[]>>;
135
+ desiredWorkloadsAsync(
136
+ context: InfraExecutionContext,
137
+ ): Promise<InfraResult<readonly InfraWorkloadSpec[]>>;
138
+ reconcileAsync(
139
+ context: InfraExecutionContext,
140
+ runtimeOutputs: readonly InfraOutput[],
141
+ ): Promise<InfraResult<InfraReconcileResult>>;
142
+ statusAsync(context: InfraExecutionContext): Promise<InfraResult<readonly InfraResourceStatus[]>>;
143
+ destroyAsync(
144
+ context: InfraExecutionContext,
145
+ request: InfraDestroyRequest,
146
+ ): Promise<InfraResult<InfraReconcileResult>>;
147
+ suspendAsync?(context: InfraExecutionContext): Promise<InfraResult<InfraReconcileResult>>;
148
+ }