@ankhorage/contracts 20.0.0 → 22.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 (105) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/appManifest/apis.d.ts +3 -3
  3. package/dist/appManifest/apis.d.ts.map +1 -1
  4. package/dist/appManifest/apis.js +4 -6
  5. package/dist/appManifest/apis.js.map +1 -1
  6. package/dist/appManifest/bindings.d.ts.map +1 -1
  7. package/dist/appManifest/bindings.js +2 -1
  8. package/dist/appManifest/bindings.js.map +1 -1
  9. package/dist/appManifest/data.d.ts.map +1 -1
  10. package/dist/appManifest/data.js +5 -4
  11. package/dist/appManifest/data.js.map +1 -1
  12. package/dist/appManifest/isAppManifest.js +3 -3
  13. package/dist/appManifest/isAppManifest.js.map +1 -1
  14. package/dist/appManifest/isManifestValue.d.ts +1 -1
  15. package/dist/appManifest/isManifestValue.d.ts.map +1 -1
  16. package/dist/appManifest/isManifestValue.js +3 -12
  17. package/dist/appManifest/isManifestValue.js.map +1 -1
  18. package/dist/appManifest/screens.d.ts +3 -2
  19. package/dist/appManifest/screens.d.ts.map +1 -1
  20. package/dist/appManifest/screens.js +15 -7
  21. package/dist/appManifest/screens.js.map +1 -1
  22. package/dist/bindings.d.ts +2 -3
  23. package/dist/bindings.d.ts.map +1 -1
  24. package/dist/bindings.js.map +1 -1
  25. package/dist/collections.d.ts +4 -0
  26. package/dist/collections.d.ts.map +1 -0
  27. package/dist/collections.js +6 -0
  28. package/dist/collections.js.map +1 -0
  29. package/dist/data/apis.d.ts +1 -1
  30. package/dist/data/apis.d.ts.map +1 -1
  31. package/dist/data/apis.js.map +1 -1
  32. package/dist/data/values.d.ts +2 -3
  33. package/dist/data/values.d.ts.map +1 -1
  34. package/dist/data/values.js.map +1 -1
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/infra/isInfraEnvironmentSpec.d.ts.map +1 -1
  40. package/dist/infra/isInfraEnvironmentSpec.js +10 -12
  41. package/dist/infra/isInfraEnvironmentSpec.js.map +1 -1
  42. package/dist/infra/isInfraManifest.d.ts.map +1 -1
  43. package/dist/infra/isInfraManifest.js +14 -5
  44. package/dist/infra/isInfraManifest.js.map +1 -1
  45. package/dist/infra/isInfraWorkloadSpec.d.ts.map +1 -1
  46. package/dist/infra/isInfraWorkloadSpec.js +26 -20
  47. package/dist/infra/isInfraWorkloadSpec.js.map +1 -1
  48. package/dist/requirements.d.ts +5 -10
  49. package/dist/requirements.d.ts.map +1 -1
  50. package/dist/requirements.js.map +1 -1
  51. package/dist/serializable.d.ts +6 -0
  52. package/dist/serializable.d.ts.map +1 -0
  53. package/dist/serializable.js +14 -0
  54. package/dist/serializable.js.map +1 -0
  55. package/dist/state.d.ts +2 -3
  56. package/dist/state.d.ts.map +1 -1
  57. package/dist/state.js.map +1 -1
  58. package/dist/types/infraAdapters.d.ts +12 -3
  59. package/dist/types/infraAdapters.d.ts.map +1 -1
  60. package/dist/types/infraAdapters.js.map +1 -1
  61. package/dist/types/infraManifest.d.ts +14 -13
  62. package/dist/types/infraManifest.d.ts.map +1 -1
  63. package/dist/types/infraManifest.js.map +1 -1
  64. package/dist/types/infraWorkload.d.ts +11 -11
  65. package/dist/types/infraWorkload.d.ts.map +1 -1
  66. package/dist/types/infraWorkload.js.map +1 -1
  67. package/dist/types.d.ts +9 -8
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js.map +1 -1
  70. package/package.json +10 -9
  71. package/src/appManifest/apis.ts +9 -6
  72. package/src/appManifest/bindings.ts +7 -1
  73. package/src/appManifest/data.ts +13 -4
  74. package/src/appManifest/isAppManifest.ts +3 -3
  75. package/src/appManifest/isManifestValue.ts +3 -16
  76. package/src/appManifest/screens.ts +21 -10
  77. package/src/appManifest.test.ts +21 -16
  78. package/src/bindings.test.ts +11 -8
  79. package/src/bindings.ts +2 -9
  80. package/src/collections.ts +8 -0
  81. package/src/contracts.test.ts +16 -13
  82. package/src/data/apis.test.ts +6 -4
  83. package/src/data/apis.ts +1 -1
  84. package/src/data/values.ts +3 -9
  85. package/src/deploy.test.ts +4 -4
  86. package/src/index.ts +2 -0
  87. package/src/infra/isInfraEnvironmentSpec.ts +16 -16
  88. package/src/infra/isInfraManifest.ts +17 -6
  89. package/src/infra/isInfraWorkloadSpec.ts +39 -24
  90. package/src/infra-published-port.test.ts +8 -8
  91. package/src/infra-types.test.ts +27 -0
  92. package/src/infra-workload-persistence.test.ts +4 -4
  93. package/src/infra-workload.test.ts +24 -22
  94. package/src/infra.test.ts +16 -20
  95. package/src/infraPersistence.test.ts +1 -1
  96. package/src/infraServices.test.ts +1 -1
  97. package/src/requirements.test.ts +2 -2
  98. package/src/requirements.ts +6 -12
  99. package/src/secrets.test.ts +1 -1
  100. package/src/serializable.ts +25 -0
  101. package/src/state.ts +3 -6
  102. package/src/types/infraAdapters.ts +20 -5
  103. package/src/types/infraManifest.ts +17 -13
  104. package/src/types/infraWorkload.ts +12 -11
  105. package/src/types.ts +11 -12
@@ -5,7 +5,7 @@ import { isInfraEnvironmentSpec, isInfraWorkloadSpec } from './infra';
5
5
  const publicWorkload = {
6
6
  id: 'api',
7
7
  artifact: { kind: 'image', image: 'example/api:1' },
8
- ports: [{ name: 'http', port: 8080, publishedPort: 18_080 }],
8
+ ports: { http: { port: 8080, publishedPort: 18_080 } },
9
9
  exposure: 'public',
10
10
  } as const;
11
11
 
@@ -17,16 +17,16 @@ describe('published workload ports', () => {
17
17
  });
18
18
 
19
19
  it.each([
20
- { ...publicWorkload, ports: [{ name: 'http', port: 80, publishedPort: 0 }] },
21
- { ...publicWorkload, ports: [{ name: 'http', port: 80, publishedPort: 65_536 }] },
20
+ { ...publicWorkload, ports: { http: { port: 80, publishedPort: 0 } } },
21
+ { ...publicWorkload, ports: { http: { port: 80, publishedPort: 65_536 } } },
22
22
  { ...publicWorkload, exposure: 'internal' },
23
23
  { ...publicWorkload, replicas: 2 },
24
24
  {
25
25
  ...publicWorkload,
26
- ports: [
27
- { name: 'http', port: 80, publishedPort: 8080 },
28
- { name: 'admin', port: 81, publishedPort: 8080 },
29
- ],
26
+ ports: {
27
+ http: { port: 80, publishedPort: 8080 },
28
+ admin: { port: 81, publishedPort: 8080 },
29
+ },
30
30
  },
31
31
  ])('rejects an invalid fixed external listener: %j', (workload) => {
32
32
  expect(isInfraWorkloadSpec(workload)).toBe(false);
@@ -36,7 +36,7 @@ describe('published workload ports', () => {
36
36
  expect(
37
37
  isInfraEnvironmentSpec({
38
38
  deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
39
- workloads: [publicWorkload, { ...publicWorkload, id: 'admin' }],
39
+ workloads: { api: publicWorkload, admin: { ...publicWorkload, id: 'admin' } },
40
40
  }),
41
41
  ).toBe(false);
42
42
  });
@@ -8,8 +8,10 @@ import type {
8
8
  InfraComputeAdapter,
9
9
  InfraComputeSnapshot,
10
10
  InfraControlPlaneCredentialRef,
11
+ InfraCredentialPort,
11
12
  InfraDeploymentSpec,
12
13
  InfraDestroyRequest,
14
+ InfraExecutionContext,
13
15
  InfraManifest,
14
16
  InfraObjectStorageSpec,
15
17
  InfraOutput,
@@ -17,6 +19,7 @@ import type {
17
19
  InfraRuntimeAdapter,
18
20
  InfraRuntimeDesiredState,
19
21
  InfraRuntimeProviderId,
22
+ InfraServiceAdapter,
20
23
  } from './infra';
21
24
  import type { AppStateSpec } from './state';
22
25
 
@@ -122,3 +125,27 @@ it('requires read-only compute discovery separately from mutating ensure', () =>
122
125
 
123
126
  expect(computeBoundary).toEqual([true, true]);
124
127
  });
128
+
129
+ it('shares one generic credential port and optional service preparation lifecycle', () => {
130
+ type CredentialBundle = Readonly<Record<string, string>>;
131
+ type Service = InfraServiceAdapter;
132
+
133
+ const boundary: readonly [
134
+ Assignable<InfraExecutionContext['credentials'], InfraCredentialPort>,
135
+ Assignable<
136
+ Awaited<ReturnType<InfraCredentialPort['findAsync']>>,
137
+ InfraResult<CredentialBundle | null>
138
+ >,
139
+ Assignable<
140
+ Awaited<ReturnType<InfraCredentialPort['resolveAsync']>>,
141
+ InfraResult<CredentialBundle>
142
+ >,
143
+ Assignable<Awaited<ReturnType<InfraCredentialPort['persistAsync']>>, InfraResult<null>>,
144
+ Assignable<
145
+ Exclude<Service['prepareAsync'], undefined>,
146
+ (context: InfraExecutionContext) => Promise<InfraResult<null>>
147
+ >,
148
+ ] = [true, true, true, true, true];
149
+
150
+ expect(boundary).toEqual([true, true, true, true, true]);
151
+ });
@@ -5,15 +5,15 @@ import { isInfraWorkloadSpec } from './infra';
5
5
  const workload = {
6
6
  id: 'database',
7
7
  artifact: { kind: 'image', image: 'example/database:1' },
8
- persistence: [
9
- {
8
+ persistence: {
9
+ config: {
10
10
  id: 'config',
11
11
  mountPath: '/etc/database-custom',
12
12
  sizeGiB: 1,
13
13
  seed: 'image',
14
14
  retention: 'retain',
15
15
  },
16
- ],
16
+ },
17
17
  } as const;
18
18
 
19
19
  it('accepts image-seeded persistence initialization', () => {
@@ -24,7 +24,7 @@ it('rejects unsupported persistence initialization modes', () => {
24
24
  expect(
25
25
  isInfraWorkloadSpec({
26
26
  ...workload,
27
- persistence: [{ ...workload.persistence[0], seed: 'empty' }],
27
+ persistence: { config: { ...workload.persistence.config, seed: 'empty' } },
28
28
  }),
29
29
  ).toBe(false);
30
30
  });
@@ -10,6 +10,13 @@ import {
10
10
  } from './infra';
11
11
  import type { AppManifest } from './types';
12
12
 
13
+ const defaultTheme = {
14
+ id: 'default',
15
+ name: 'Default',
16
+ light: { primaryColor: '#3366ff', harmony: 'analogous' },
17
+ dark: { primaryColor: '#6699ff', harmony: 'analogous' },
18
+ } as const;
19
+
13
20
  const reference = {
14
21
  source: 'secret-store',
15
22
  projectId: 'example',
@@ -22,7 +29,7 @@ const workload = {
22
29
  artifact: { kind: 'image', image: 'example/backend@sha256:abc' },
23
30
  command: ['server'],
24
31
  args: ['--verbose'],
25
- ports: [{ name: 'http', port: 8080, publishedPort: 18_080 }],
32
+ ports: { http: { port: 8080, publishedPort: 18_080 } },
26
33
  environment: {
27
34
  NODE_ENV: { kind: 'literal', value: 'production' },
28
35
  DB_PASSWORD: { kind: 'secret', reference },
@@ -45,36 +52,31 @@ const workload = {
45
52
  ],
46
53
  },
47
54
  },
48
- files: [{ path: '/etc/backend/config.json', content: { kind: 'literal', value: '{}' } }],
55
+ files: { '/etc/backend/config.json': { kind: 'literal', value: '{}' } },
49
56
  health: { kind: 'http', port: 8080, path: '/health', intervalSeconds: 5 },
50
57
  resources: { cpuMillis: 500, memoryMiB: 256 },
51
- persistence: [{ id: 'data', mountPath: '/data', sizeGiB: 10, retention: 'retain' }],
58
+ persistence: { data: { id: 'data', mountPath: '/data', sizeGiB: 10, retention: 'retain' } },
52
59
  exposure: 'public',
53
60
  replicas: 1,
54
- dependsOn: ['database'],
61
+ dependsOn: { database: true },
55
62
  } as const satisfies InfraWorkloadSpec;
56
63
 
57
64
  const invalidWorkloads = [
58
65
  { artifact: { kind: 'build', dockerfile: 'Dockerfile' } },
59
66
  { artifact: { kind: 'image', image: '' } },
60
- { ports: [{ name: 'http', port: 0 }] },
61
- { ports: [{ name: 'http', port: 65536 }] },
62
- { ports: [{ name: 'http', port: 1.5 }] },
63
- { ports: [{ name: 'http', port: 80, protocol: 'sctp' }] },
64
- {
65
- ports: [
66
- { name: 'http', port: 80 },
67
- { name: 'http', port: 81 },
68
- ],
69
- },
67
+ { ports: { http: { port: 0 } } },
68
+ { ports: { http: { port: 65536 } } },
69
+ { ports: { http: { port: 1.5 } } },
70
+ { ports: { http: { port: 80, protocol: 'sctp' } } },
71
+ { ports: { '': { port: 80 } } },
70
72
  { replicas: -1 },
71
73
  { replicas: NaN },
72
74
  { replicas: 1.5 },
73
75
  { resources: { memoryMiB: Infinity } },
74
76
  { resources: { cpuMillis: 0 } },
75
- { persistence: [{ id: 'data', mountPath: '/data', sizeGiB: 1 }] },
76
- { persistence: [{ id: 'data', mountPath: '../data', sizeGiB: 1, retention: 'retain' }] },
77
- { files: [{ path: '/etc/../secret', content: { kind: 'literal', value: 'x' } }] },
77
+ { persistence: { data: { id: 'data', mountPath: '/data', sizeGiB: 1 } } },
78
+ { persistence: { data: { id: 'data', mountPath: '../data', sizeGiB: 1, retention: 'retain' } } },
79
+ { files: { '/etc/../secret': { kind: 'literal', value: 'x' } } },
78
80
  { health: { kind: 'http', port: 80, path: '/health', failureThreshold: 0 } },
79
81
  { health: { kind: 'tcp', port: 80, path: '/health' } },
80
82
  { health: { kind: 'command', command: [] } },
@@ -134,7 +136,7 @@ describe('runtime-neutral workload boundary', () => {
134
136
  expect(
135
137
  isInfraEnvironmentSpec({
136
138
  deployment: { compute: { provider: 'local' }, runtime: { provider } },
137
- workloads: [workload],
139
+ workloads: { backend: workload },
138
140
  }),
139
141
  ).toBe(true);
140
142
  }
@@ -156,11 +158,11 @@ describe('runtime-neutral workload boundary', () => {
156
158
  ).toBe(true);
157
159
  });
158
160
 
159
- it('rejects duplicate workload identities', () => {
161
+ it('rejects workload registries whose key disagrees with workload id', () => {
160
162
  expect(
161
163
  isInfraEnvironmentSpec({
162
164
  deployment: { compute: { provider: 'local' }, runtime: { provider: 'k3s' } },
163
- workloads: [workload, workload],
165
+ workloads: { duplicate: workload },
164
166
  }),
165
167
  ).toBe(false);
166
168
  });
@@ -266,7 +268,7 @@ describe('application state boundary', () => {
266
268
  category: 'developer_tools',
267
269
  themeId: 'default',
268
270
  },
269
- themes: [],
271
+ themes: { default: defaultTheme },
270
272
  activeThemeId: 'default',
271
273
  screens: {},
272
274
  navigator: { type: 'stack', routes: [] },
@@ -276,7 +278,7 @@ describe('application state boundary', () => {
276
278
  deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
277
279
  },
278
280
  },
279
- modules: [],
281
+ modules: {},
280
282
  },
281
283
  settings: { localization: { defaultLocale: 'en', locales: ['en'] } },
282
284
  } satisfies AppManifest;
package/src/infra.test.ts CHANGED
@@ -23,7 +23,7 @@ const production = {
23
23
  },
24
24
  database: { provider: 'supabase' },
25
25
  auth: { provider: 'supabase' },
26
- objectStorage: { provider: 'r2', buckets: ['media'] },
26
+ objectStorage: { provider: 'r2', buckets: { media: true } },
27
27
  authz: { provider: 'cerbos', kind: 'ABAC' },
28
28
  secretStore: { provider: 'supabase-vault' },
29
29
  networking: { domain: 'api.example.ch', publicBaseUrl: 'https://api.example.ch' },
@@ -33,7 +33,7 @@ describe('standalone Infra environments', () => {
33
33
  it('parses local/preview/production without an AppManifest or Deploy', () => {
34
34
  const manifest = {
35
35
  environments: { local, preview: production, production },
36
- modules: [],
36
+ modules: {},
37
37
  } satisfies InfraManifest;
38
38
  expect(parseInfraManifest(JSON.parse(JSON.stringify(manifest)))).toEqual({
39
39
  ok: true,
@@ -45,14 +45,14 @@ describe('standalone Infra environments', () => {
45
45
  it.each([
46
46
  null,
47
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' },
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: { invalid: 42 } },
54
+ { environments: { local }, modules: {}, modulesConfig: {} },
55
+ { environments: { local }, modules: {}, apis: 'invalid' },
56
56
  ])('rejects invalid environment boundaries: %j', (value) => {
57
57
  expect(isInfraManifest(value)).toBe(false);
58
58
  expect(parseInfraManifest(value).ok).toBe(false);
@@ -69,7 +69,7 @@ describe('standalone Infra environments', () => {
69
69
  'plugins',
70
70
  'pluginsConfig',
71
71
  ])('rejects removed top-level %s', (key) => {
72
- expect(isInfraManifest({ environments: { local }, modules: [], [key]: {} })).toBe(false);
72
+ expect(isInfraManifest({ environments: { local }, modules: {}, [key]: {} })).toBe(false);
73
73
  });
74
74
 
75
75
  it.each(['storage', 'state', 'monitoring', 'observability', 'target'])(
@@ -148,21 +148,17 @@ describe('Cerbos policy configuration', () => {
148
148
  authz: {
149
149
  provider: 'cerbos',
150
150
  kind: 'ABAC',
151
- policies: [{ path: 'app.yaml', content: 'apiVersion: api.cerbos.dev/v1' }],
151
+ policies: { 'app.yaml': 'apiVersion: api.cerbos.dev/v1' },
152
152
  },
153
153
  }),
154
154
  ).toBe(true);
155
155
  });
156
156
 
157
157
  it.each([
158
- { policies: [{ path: '../app.yaml', content: 'x' }] },
159
- {
160
- policies: [
161
- { path: 'app.yaml', content: 'x' },
162
- { path: 'app.yaml', content: 'y' },
163
- ],
164
- },
165
- ])('rejects unsafe or duplicate policy files: %j', ({ policies }) => {
158
+ { policies: { '../app.yaml': 'x' } },
159
+ { policies: { 'app.yaml': 42 } },
160
+ { policies: [{ path: 'app.yaml', content: 'legacy-array-shape' }] },
161
+ ])('rejects unsafe or non-canonical policy files: %j', ({ policies }) => {
166
162
  expect(
167
163
  isInfraEnvironmentSpec({
168
164
  ...local,
@@ -34,7 +34,7 @@ it('accepts scheduled database backups and an S3-backed Supabase storage service
34
34
  },
35
35
  objectStorage: {
36
36
  provider: 'supabase',
37
- buckets: ['media'],
37
+ buckets: { media: true },
38
38
  backend: { ...target, bucket: 'ankhorage-storage' },
39
39
  },
40
40
  }),
@@ -12,7 +12,7 @@ const production = {
12
12
  },
13
13
  database: { provider: 'supabase' },
14
14
  auth: { provider: 'supabase' },
15
- objectStorage: { provider: 'r2', buckets: ['media'] },
15
+ objectStorage: { provider: 'r2', buckets: { media: true } },
16
16
  authz: { provider: 'cerbos', kind: 'ABAC' },
17
17
  secretStore: { provider: 'supabase-vault' },
18
18
  networking: { domain: 'api.example.ch', publicBaseUrl: 'https://api.example.ch' },
@@ -11,10 +11,10 @@ describe('platform requirements', () => {
11
11
  it('exports ebookReader as the canonical reader capability', () => {
12
12
  const capability: AnkhorageCapabilityName = 'ebookReader';
13
13
  const componentRequirements: ComponentRequirements = {
14
- capabilities: [{ capability }],
14
+ capabilities: { [capability]: true },
15
15
  };
16
16
  const screenRequirements: ScreenRequirements = {
17
- capabilities: [{ capability }],
17
+ capabilities: { [capability]: true },
18
18
  };
19
19
 
20
20
  expect(ANKHORAGE_CAPABILITY_NAMES).toContain(capability);
@@ -1,3 +1,5 @@
1
+ import type { SerializableSet } from './collections';
2
+
1
3
  export const ANKHORAGE_PERMISSION_NAMES = [
2
4
  'camera',
3
5
  'microphone',
@@ -24,20 +26,12 @@ export const ANKHORAGE_CAPABILITY_NAMES = [
24
26
 
25
27
  export type AnkhorageCapabilityName = (typeof ANKHORAGE_CAPABILITY_NAMES)[number];
26
28
 
27
- export interface ScreenPermissionRequirement {
28
- readonly permission: AnkhoragePermissionName;
29
- }
30
-
31
- export interface ScreenCapabilityRequirement {
32
- readonly capability: AnkhorageCapabilityName;
33
- }
34
-
35
29
  export interface ScreenRequirements {
36
- readonly permissions?: readonly ScreenPermissionRequirement[];
37
- readonly capabilities?: readonly ScreenCapabilityRequirement[];
30
+ readonly permissions?: SerializableSet<AnkhoragePermissionName>;
31
+ readonly capabilities?: SerializableSet<AnkhorageCapabilityName>;
38
32
  }
39
33
 
40
34
  export interface ComponentRequirements {
41
- readonly permissions?: readonly ScreenPermissionRequirement[];
42
- readonly capabilities?: readonly ScreenCapabilityRequirement[];
35
+ readonly permissions?: SerializableSet<AnkhoragePermissionName>;
36
+ readonly capabilities?: SerializableSet<AnkhorageCapabilityName>;
43
37
  }
@@ -67,7 +67,7 @@ describe('secret-store contracts', () => {
67
67
  };
68
68
 
69
69
  const infra: InfraManifest = {
70
- modules: [],
70
+ modules: {},
71
71
  environments: {
72
72
  local: {
73
73
  deployment: { compute: { provider: 'local' }, runtime: { provider: 'minikube' } },
@@ -0,0 +1,25 @@
1
+ import { isRecord } from '@ankhorage/utility/object';
2
+
3
+ export type SerializableValue =
4
+ | string
5
+ | number
6
+ | boolean
7
+ | null
8
+ | readonly SerializableValue[]
9
+ | {
10
+ readonly [key: string]: SerializableValue;
11
+ };
12
+
13
+ /*** Validate recursively serializable manifest/config values without accepting functions or host objects. */
14
+ export function isSerializableValue(value: unknown): value is SerializableValue {
15
+ if (
16
+ value === null ||
17
+ typeof value === 'string' ||
18
+ typeof value === 'number' ||
19
+ typeof value === 'boolean'
20
+ ) {
21
+ return true;
22
+ }
23
+ if (Array.isArray(value)) return value.every(isSerializableValue);
24
+ return isRecord(value) && Object.values(value).every(isSerializableValue);
25
+ }
package/src/state.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { SerializableValue } from './serializable';
2
+
1
3
  /** Legend currently implements in-memory state only; unsupported persistence cannot be selected. */
2
4
  export interface AppStateSpec {
3
5
  readonly provider: 'legend';
@@ -6,12 +8,7 @@ export interface AppStateSpec {
6
8
 
7
9
  export type StatePrimitive = string | number | boolean | null;
8
10
 
9
- export type StateValue =
10
- | StatePrimitive
11
- | readonly StateValue[]
12
- | {
13
- readonly [key: string]: StateValue;
14
- };
11
+ export type StateValue = SerializableValue;
15
12
 
16
13
  export type StatePath = string | readonly string[];
17
14
 
@@ -29,6 +29,23 @@ export type InfraAdapterDescriptor<P extends InfraAdapterId = InfraAdapterId> =
29
29
  };
30
30
  }[P];
31
31
 
32
+ /** Trusted execution-only control-plane credential lookup, resolution and persistence. */
33
+ export interface InfraCredentialPort {
34
+ /** Return a credential bundle when present; absence is not an error. */
35
+ findAsync(
36
+ reference: InfraControlPlaneCredentialRef,
37
+ ): Promise<InfraResult<Readonly<Record<string, string>> | null>>;
38
+ /** Resolve a required credential bundle; absence is an error. */
39
+ resolveAsync(
40
+ reference: InfraControlPlaneCredentialRef,
41
+ ): Promise<InfraResult<Readonly<Record<string, string>>>>;
42
+ /** Persist an opaque provider-owned credential bundle without exposing its schema to the host. */
43
+ persistAsync(
44
+ reference: InfraControlPlaneCredentialRef,
45
+ values: Readonly<Record<string, string>>,
46
+ ): Promise<InfraResult<null>>;
47
+ }
48
+
32
49
  /** Trusted execution-only ports. Implementations must not serialize resolved credential values. */
33
50
  export interface InfraExecutionContext {
34
51
  readonly projectId: string;
@@ -36,11 +53,7 @@ export interface InfraExecutionContext {
36
53
  readonly desired: InfraEnvironmentSpec;
37
54
  readonly previous?: InfraLedger;
38
55
  readonly signal?: AbortSignal;
39
- readonly credentials: {
40
- resolveAsync(
41
- reference: InfraControlPlaneCredentialRef,
42
- ): Promise<InfraResult<Readonly<Record<string, string>>>>;
43
- };
56
+ readonly credentials: InfraCredentialPort;
44
57
  readonly secrets: {
45
58
  resolveAsync(reference: InfraSecretReference): Promise<InfraResult<string>>;
46
59
  };
@@ -133,6 +146,8 @@ export interface InfraServiceAdapter {
133
146
  /** Read-only service config, bootstrap credentials and dependency validation. */
134
147
  validateAsync(context: InfraExecutionContext): Promise<InfraResult<null>>;
135
148
  planAsync(context: InfraExecutionContext): Promise<InfraResult<readonly InfraPlanAction[]>>;
149
+ /** Mutating pre-runtime preparation invoked only by an orchestrated `up` operation. */
150
+ prepareAsync?(context: InfraExecutionContext): Promise<InfraResult<null>>;
136
151
  desiredWorkloadsAsync(
137
152
  context: InfraExecutionContext,
138
153
  ): Promise<InfraResult<readonly InfraWorkloadSpec[]>>;
@@ -1,10 +1,11 @@
1
1
  import type { AuthFlowConfig, AuthOAuthConfig } from '../auth';
2
- import type { ApiDefinitionList } from '../data';
2
+ import type { SerializableSet } from '../collections';
3
+ import type { ApiDefinitionRegistry, DataContractValue } from '../data';
3
4
  import type { AppEnvironmentId } from '../environments';
4
5
  import type { INFRA_ADAPTER_CATALOG, INFRA_RUNTIME_COMPATIBILITY } from '../infra/constants';
5
6
  import type { AuthProfileSpec, AuthScope, AuthSignInSpec, AuthSignUpSpec } from '../types';
6
7
  import type { InfraControlPlaneCredentialRef } from './infraSecrets';
7
- import type { InfraWorkloadSpec } from './infraWorkload';
8
+ import type { InfraWorkloadRegistry } from './infraWorkload';
8
9
 
9
10
  export type InfraAdapterId = keyof typeof INFRA_ADAPTER_CATALOG;
10
11
  export type InfraCapability =
@@ -86,11 +87,11 @@ export interface InfraDatabaseConfigMap {
86
87
  }
87
88
  export interface InfraObjectStorageConfigMap {
88
89
  readonly supabase: {
89
- readonly buckets?: readonly string[];
90
+ readonly buckets?: SerializableSet;
90
91
  readonly backend?: InfraS3PersistenceTarget;
91
92
  };
92
93
  readonly r2: {
93
- readonly buckets?: readonly string[];
94
+ readonly buckets?: SerializableSet;
94
95
  readonly accountId?: string;
95
96
  readonly credentials?: InfraControlPlaneCredentialRef;
96
97
  };
@@ -108,13 +109,9 @@ export interface InfraAuthConfigMap {
108
109
  export interface InfraAuthzConfigMap {
109
110
  readonly cerbos: {
110
111
  readonly kind: 'RBAC' | 'ABAC';
111
- readonly policies?: readonly InfraAuthzPolicyFile[];
112
+ readonly policies?: Readonly<Record<string, string>>;
112
113
  };
113
114
  }
114
- export interface InfraAuthzPolicyFile {
115
- readonly path: string;
116
- readonly content: string;
117
- }
118
115
  export interface InfraSecretStoreConfigMap {
119
116
  readonly 'supabase-vault': { readonly schema?: string };
120
117
  }
@@ -161,15 +158,22 @@ export interface InfraEnvironmentSpec {
161
158
  readonly authz?: InfraAuthzSpec;
162
159
  readonly secretStore?: InfraSecretStoreSpec;
163
160
  readonly networking?: InfraNetworkingSpec;
164
- readonly workloads?: readonly InfraWorkloadSpec[];
161
+ readonly workloads?: InfraWorkloadRegistry;
165
162
  }
166
163
 
164
+ export type InfraModuleId = string;
165
+
166
+ export interface InfraModuleSpec {
167
+ readonly config?: DataContractValue;
168
+ }
169
+
170
+ export type InfraModuleRegistry = Readonly<Record<InfraModuleId, InfraModuleSpec>>;
171
+
167
172
  export interface InfraManifest {
168
173
  readonly environments: Readonly<
169
174
  Record<'local', InfraEnvironmentSpec> &
170
175
  Partial<Record<Exclude<AppEnvironmentId, 'local'>, InfraEnvironmentSpec>>
171
176
  >;
172
- readonly apis?: ApiDefinitionList;
173
- readonly modules: readonly string[];
174
- readonly modulesConfig?: Readonly<Record<string, unknown>>;
177
+ readonly apis?: ApiDefinitionRegistry;
178
+ readonly modules: InfraModuleRegistry;
175
179
  }
@@ -1,3 +1,4 @@
1
+ import type { SerializableSet } from '../collections';
1
2
  import type { InfraControlPlaneCredentialRef, InfraSecretReference } from './infraSecrets';
2
3
 
3
4
  export interface InfraWorkloadArtifact {
@@ -25,7 +26,6 @@ export type InfraWorkloadValue =
25
26
  };
26
27
 
27
28
  export interface InfraWorkloadPort {
28
- readonly name: string;
29
29
  readonly port: number;
30
30
  readonly protocol?: 'tcp' | 'udp';
31
31
  /** Exact external listener port requested for a public single-replica workload. */
@@ -57,25 +57,26 @@ export interface InfraWorkloadVolumeSpec {
57
57
  readonly retention: 'retain' | 'delete-on-destroy';
58
58
  }
59
59
 
60
- /** Portable config/policy files; secret values are materialized by the runtime. */
61
- export interface InfraWorkloadFileSpec {
62
- readonly path: string;
63
- readonly content: InfraWorkloadValue;
64
- }
60
+ export type InfraWorkloadId = string;
61
+ export type InfraWorkloadPortRegistry = Readonly<Record<string, InfraWorkloadPort>>;
62
+ export type InfraWorkloadFileMap = Readonly<Record<string, InfraWorkloadValue>>;
63
+ export type InfraWorkloadVolumeRegistry = Readonly<Record<string, InfraWorkloadVolumeSpec>>;
65
64
 
66
65
  export interface InfraWorkloadSpec {
67
- readonly id: string;
66
+ readonly id: InfraWorkloadId;
68
67
  readonly artifact: InfraWorkloadArtifact;
69
68
  readonly command?: readonly string[];
70
69
  readonly args?: readonly string[];
71
- readonly ports?: readonly InfraWorkloadPort[];
70
+ readonly ports?: InfraWorkloadPortRegistry;
72
71
  readonly environment?: Readonly<Record<string, InfraWorkloadValue>>;
73
- readonly files?: readonly InfraWorkloadFileSpec[];
72
+ readonly files?: InfraWorkloadFileMap;
74
73
  readonly health?: InfraWorkloadHealthSpec;
75
74
  readonly resources?: InfraWorkloadResourceSpec;
76
- readonly persistence?: readonly InfraWorkloadVolumeSpec[];
75
+ readonly persistence?: InfraWorkloadVolumeRegistry;
77
76
  readonly exposure?: 'internal' | 'public';
78
77
  readonly replicas?: number;
79
78
  /** IDs in the composed desired-state graph; the orchestrator validates missing/cyclic edges. */
80
- readonly dependsOn?: readonly string[];
79
+ readonly dependsOn?: SerializableSet;
81
80
  }
81
+
82
+ export type InfraWorkloadRegistry = Readonly<Record<InfraWorkloadId, InfraWorkloadSpec>>;
package/src/types.ts CHANGED
@@ -14,6 +14,7 @@ import type { MediaAssetReference, MediaManifest } from './media';
14
14
  import type { AppNavigatorManifest } from './navigator';
15
15
  import type { RepositoryManifest } from './repository';
16
16
  import type { ScreenRequirements } from './requirements';
17
+ import type { SerializableValue } from './serializable';
17
18
  import type { AppStateSpec } from './state';
18
19
  import type { ThemeGlobalTokenOverrides, ThemeRecipeOverrides } from './theme';
19
20
 
@@ -22,8 +23,10 @@ export interface ThemeModeConfig {
22
23
  harmony: ColorHarmony;
23
24
  }
24
25
 
26
+ export type ThemeId = string;
27
+
25
28
  export interface ThemeConfig {
26
- id: string;
29
+ id: ThemeId;
27
30
  name: string;
28
31
  light: ThemeModeConfig;
29
32
  dark: ThemeModeConfig;
@@ -33,6 +36,8 @@ export interface ThemeConfig {
33
36
  recipes?: ThemeRecipeOverrides;
34
37
  }
35
38
 
39
+ export type ThemeRegistry = Readonly<Record<ThemeId, ThemeConfig>>;
40
+
36
41
  export type ActionType =
37
42
  'navigate' | 'alert' | 'console' | 'toggleDarkMode' | 'setLanguage' | 'search' | 'filter';
38
43
 
@@ -93,13 +98,7 @@ export type Action =
93
98
  | SetLanguageAction
94
99
  | ToggleDarkModeAction;
95
100
 
96
- export type ManifestValue =
97
- | string
98
- | number
99
- | boolean
100
- | null
101
- | readonly ManifestValue[]
102
- | { readonly [key: string]: ManifestValue };
101
+ export type ManifestValue = SerializableValue;
103
102
 
104
103
  export type ComponentEventPayloadValue = ManifestValue;
105
104
 
@@ -271,12 +270,12 @@ export interface AppManifest {
271
270
  slug: string;
272
271
  version: string;
273
272
  category: AppCategory;
274
- themeId: string;
273
+ themeId: ThemeId;
275
274
  created?: string;
276
275
  updated?: string;
277
276
  };
278
- themes: ThemeConfig[];
279
- activeThemeId: string;
277
+ themes: ThemeRegistry;
278
+ activeThemeId: ThemeId;
280
279
  activeThemeMode?: 'dark' | 'light';
281
280
  splashScreen?: SplashScreenSpec;
282
281
  /** Studio-managed authoring media. Runtime/user uploads are intentionally separate. */
@@ -287,7 +286,7 @@ export interface AppManifest {
287
286
  state?: AppStateSpec;
288
287
  infra: InfraManifest;
289
288
  navigator: AppNavigatorManifest;
290
- screens: Record<string, ScreenSpec>;
289
+ screens: Readonly<Record<string, ScreenSpec>>;
291
290
  dataSources?: DataSourceRegistry;
292
291
  dataBindings?: ComponentDataBindingRegistry;
293
292
  repository?: RepositoryManifest;