@volter/world-runtime 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/known-external-services.json +1108 -0
- package/dist/src/ancestry.d.ts +2 -0
- package/dist/src/ancestry.js +42 -0
- package/dist/src/app-url.d.ts +47 -0
- package/dist/src/app-url.js +239 -0
- package/dist/src/attach.d.ts +48 -0
- package/dist/src/attach.js +87 -0
- package/dist/src/branch.d.ts +20 -0
- package/dist/src/branch.js +65 -0
- package/dist/src/browser-proxy-cli.d.ts +2 -0
- package/dist/src/browser-proxy-cli.js +41 -0
- package/dist/src/ca-trust.d.ts +5 -0
- package/dist/src/ca-trust.js +64 -0
- package/dist/src/catalog.d.ts +31 -0
- package/dist/src/catalog.js +148 -0
- package/dist/src/changeset.d.ts +142 -0
- package/dist/src/changeset.js +570 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +1262 -0
- package/dist/src/command-lifetime.d.ts +15 -0
- package/dist/src/command-lifetime.js +98 -0
- package/dist/src/configs.d.ts +18 -0
- package/dist/src/configs.js +119 -0
- package/dist/src/console-apart.d.ts +38 -0
- package/dist/src/console-apart.js +107 -0
- package/dist/src/consumers.d.ts +46 -0
- package/dist/src/consumers.js +200 -0
- package/dist/src/covers.d.ts +183 -0
- package/dist/src/covers.js +800 -0
- package/dist/src/fixture-env.d.ts +42 -0
- package/dist/src/fixture-env.js +221 -0
- package/dist/src/host-cli.d.ts +2 -0
- package/dist/src/host-cli.js +92 -0
- package/dist/src/host-fault-fixture.d.ts +32 -0
- package/dist/src/host-fault-fixture.js +100 -0
- package/dist/src/host-worker.d.ts +1 -0
- package/dist/src/host-worker.js +23 -0
- package/dist/src/host.d.ts +38 -0
- package/dist/src/host.js +135 -0
- package/dist/src/index.d.ts +48 -0
- package/dist/src/index.js +35 -0
- package/dist/src/infra-cli.d.ts +2 -0
- package/dist/src/infra-cli.js +136 -0
- package/dist/src/init.d.ts +227 -0
- package/dist/src/init.js +1117 -0
- package/dist/src/inject-map.d.ts +34 -0
- package/dist/src/inject-map.js +56 -0
- package/dist/src/lifecycle-record.d.ts +47 -0
- package/dist/src/lifecycle-record.js +196 -0
- package/dist/src/origin.d.ts +31 -0
- package/dist/src/origin.js +139 -0
- package/dist/src/pack-facts.d.ts +75 -0
- package/dist/src/pack-facts.js +98 -0
- package/dist/src/pglite-backing.d.ts +21 -0
- package/dist/src/pglite-backing.js +158 -0
- package/dist/src/pglite-host.mjs +147 -0
- package/dist/src/placeholder.d.ts +20 -0
- package/dist/src/placeholder.js +100 -0
- package/dist/src/prerequisites.d.ts +21 -0
- package/dist/src/prerequisites.js +49 -0
- package/dist/src/process-groups.d.ts +4 -0
- package/dist/src/process-groups.js +49 -0
- package/dist/src/project-inspect.d.ts +109 -0
- package/dist/src/project-inspect.js +827 -0
- package/dist/src/proxy-daemon.d.ts +2 -0
- package/dist/src/proxy-daemon.js +18 -0
- package/dist/src/redirect-proxy.d.ts +105 -0
- package/dist/src/redirect-proxy.js +665 -0
- package/dist/src/reflect.d.ts +74 -0
- package/dist/src/reflect.js +392 -0
- package/dist/src/resources.d.ts +26 -0
- package/dist/src/resources.js +22 -0
- package/dist/src/root.d.ts +114 -0
- package/dist/src/root.js +312 -0
- package/dist/src/run-task-worker.d.ts +1 -0
- package/dist/src/run-task-worker.js +38 -0
- package/dist/src/run-task.d.ts +18 -0
- package/dist/src/run-task.js +48 -0
- package/dist/src/runtime-test-support.d.ts +59 -0
- package/dist/src/runtime-test-support.js +205 -0
- package/dist/src/runtime.d.ts +256 -0
- package/dist/src/runtime.js +3502 -0
- package/dist/src/schema.d.ts +449 -0
- package/dist/src/schema.js +605 -0
- package/dist/src/serve.d.ts +30 -0
- package/dist/src/serve.js +82 -0
- package/dist/src/served-world.d.ts +194 -0
- package/dist/src/served-world.js +986 -0
- package/dist/src/service-exit.d.ts +46 -0
- package/dist/src/service-exit.js +195 -0
- package/dist/src/service-recorder.d.ts +1 -0
- package/dist/src/service-recorder.js +121 -0
- package/dist/src/sibling.d.ts +1 -0
- package/dist/src/sibling.js +9 -0
- package/dist/src/signals.d.ts +1 -0
- package/dist/src/signals.js +11 -0
- package/dist/src/storage-capacity.d.ts +8 -0
- package/dist/src/storage-capacity.js +61 -0
- package/dist/src/tail.d.ts +30 -0
- package/dist/src/tail.js +160 -0
- package/dist/src/tcp-port.d.ts +2 -0
- package/dist/src/tcp-port.js +36 -0
- package/dist/src/up-task-worker.d.ts +1 -0
- package/dist/src/up-task-worker.js +61 -0
- package/dist/src/up-task.d.ts +17 -0
- package/dist/src/up-task.js +49 -0
- package/dist/src/websocket-relay.d.ts +3 -0
- package/dist/src/websocket-relay.js +40 -0
- package/known-external-services.json +1108 -0
- package/package.json +83 -0
- package/src/ancestry.ts +36 -0
- package/src/app-url.ts +253 -0
- package/src/attach.ts +117 -0
- package/src/branch.ts +63 -0
- package/src/browser-proxy-cli.ts +44 -0
- package/src/ca-trust.ts +57 -0
- package/src/catalog.ts +156 -0
- package/src/changeset.ts +627 -0
- package/src/cli.ts +1111 -0
- package/src/command-lifetime.ts +79 -0
- package/src/configs.ts +110 -0
- package/src/console-apart.ts +90 -0
- package/src/consumers.ts +185 -0
- package/src/covers.ts +934 -0
- package/src/fixture-env.ts +230 -0
- package/src/host-cli.ts +90 -0
- package/src/host-worker.ts +23 -0
- package/src/host.ts +169 -0
- package/src/index.ts +171 -0
- package/src/infra-cli.ts +133 -0
- package/src/init.ts +1316 -0
- package/src/inject-map.ts +72 -0
- package/src/lifecycle-record.ts +168 -0
- package/src/origin.ts +134 -0
- package/src/pack-facts.ts +128 -0
- package/src/pglite-backing.ts +141 -0
- package/src/pglite-host.mjs +147 -0
- package/src/placeholder.ts +89 -0
- package/src/prerequisites.ts +66 -0
- package/src/process-groups.ts +33 -0
- package/src/project-inspect.ts +770 -0
- package/src/proxy-daemon.ts +21 -0
- package/src/redirect-proxy.ts +684 -0
- package/src/reflect.ts +440 -0
- package/src/resources.ts +22 -0
- package/src/root.ts +290 -0
- package/src/run-task-worker.ts +27 -0
- package/src/run-task.ts +44 -0
- package/src/runtime-test-support.ts +208 -0
- package/src/runtime.ts +3357 -0
- package/src/schema.ts +922 -0
- package/src/serve.ts +102 -0
- package/src/served-world.ts +812 -0
- package/src/service-exit.ts +175 -0
- package/src/service-recorder.ts +89 -0
- package/src/sibling.ts +10 -0
- package/src/signals.ts +10 -0
- package/src/storage-capacity.ts +60 -0
- package/src/tail.ts +205 -0
- package/src/tcp-port.ts +35 -0
- package/src/up-task-worker.ts +40 -0
- package/src/up-task.ts +45 -0
- package/src/websocket-relay.ts +32 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import { normalizeWorldNetwork } from '@volter/world-core/network-policy';
|
|
2
|
+
export const WORLD_SCHEMA_VERSION = 2;
|
|
3
|
+
export const WORLD_USAGES = ['application', 'dependencies', 'build', 'deployment'];
|
|
4
|
+
/** The env value `init` writes for a name whose fake must be minted per instance (a service-account JSON with a real throwaway key). */
|
|
5
|
+
export const MINT_ENV = '$mint';
|
|
6
|
+
function assertStringArray(value, what, path) {
|
|
7
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string')) {
|
|
8
|
+
throw new Error(`${what} must be a non-empty string array in ${path}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function assertValidRegex(pattern, what, path) {
|
|
12
|
+
try {
|
|
13
|
+
new RegExp(pattern);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
throw new Error(`${what} has an invalid regex (${error instanceof Error ? error.message : String(error)}) in ${path}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Shared validator for a readiness probe — used by both `external.readyWhen` and a process/twin
|
|
20
|
+
* service's `ready` object. Exactly one of command/httpUrl/stdoutMatch, correct types, and any
|
|
21
|
+
* stdoutMatch regex must compile (so a typo fails at config time, not mid-boot). */
|
|
22
|
+
function assertReadyWhen(ready, what, path) {
|
|
23
|
+
if (!ready || typeof ready !== 'object' || Array.isArray(ready)) {
|
|
24
|
+
throw new Error(`${what} must be an object in ${path}`);
|
|
25
|
+
}
|
|
26
|
+
assertKeys(ready, ['command', 'args', 'httpUrl', 'stdoutMatch', 'timeoutMs', 'intervalMs'], what, path);
|
|
27
|
+
const r = ready;
|
|
28
|
+
const probes = [r.command !== undefined, r.httpUrl !== undefined, r.stdoutMatch !== undefined].filter(Boolean).length;
|
|
29
|
+
if (probes !== 1) {
|
|
30
|
+
throw new Error(`${what} must set exactly one of command/httpUrl/stdoutMatch in ${path}`);
|
|
31
|
+
}
|
|
32
|
+
if (r.command !== undefined && typeof r.command !== 'string')
|
|
33
|
+
throw new Error(`${what}.command must be a string in ${path}`);
|
|
34
|
+
if (r.args !== undefined && (!Array.isArray(r.args) || r.args.some((arg) => typeof arg !== 'string')))
|
|
35
|
+
throw new Error(`${what}.args must be a string array in ${path}`);
|
|
36
|
+
if (r.args !== undefined && r.command === undefined)
|
|
37
|
+
throw new Error(`${what}.args requires command in ${path}`);
|
|
38
|
+
if (r.httpUrl !== undefined && typeof r.httpUrl !== 'string')
|
|
39
|
+
throw new Error(`${what}.httpUrl must be a string in ${path}`);
|
|
40
|
+
if (r.stdoutMatch !== undefined) {
|
|
41
|
+
if (typeof r.stdoutMatch !== 'string')
|
|
42
|
+
throw new Error(`${what}.stdoutMatch must be a string in ${path}`);
|
|
43
|
+
assertValidRegex(r.stdoutMatch, `${what}.stdoutMatch`, path);
|
|
44
|
+
}
|
|
45
|
+
for (const key of ['timeoutMs', 'intervalMs']) {
|
|
46
|
+
if (r[key] !== undefined && (!Number.isInteger(r[key]) || r[key] <= 0))
|
|
47
|
+
throw new Error(`${what}.${key} must be a positive integer in ${path}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function assertExternalServiceConfig(service, path) {
|
|
51
|
+
const id = service.id;
|
|
52
|
+
if (service.command !== undefined) {
|
|
53
|
+
throw new Error(`External service "${id}" must not set "command" (use external.up/down) in ${path}`);
|
|
54
|
+
}
|
|
55
|
+
if (service.port !== undefined) {
|
|
56
|
+
throw new Error(`External service "${id}" must not set "port" — the external tool manages its own ports in ${path}`);
|
|
57
|
+
}
|
|
58
|
+
if (service.portReason !== undefined) {
|
|
59
|
+
throw new Error(`External service "${id}" must not set "portReason" — it never sets a literal "port" in ${path}`);
|
|
60
|
+
}
|
|
61
|
+
const external = service.external;
|
|
62
|
+
if (!external || typeof external !== 'object' || Array.isArray(external)) {
|
|
63
|
+
throw new Error(`External service "${id}" must define an "external" object in ${path}`);
|
|
64
|
+
}
|
|
65
|
+
assertKeys(external, ['up', 'status', 'down', 'discover', 'readyWhen'], `External service "${id}" external`, path);
|
|
66
|
+
assertStringArray(external.up, `External service "${id}" external.up`, path);
|
|
67
|
+
assertStringArray(external.down, `External service "${id}" external.down`, path);
|
|
68
|
+
if (external.status !== undefined)
|
|
69
|
+
assertStringArray(external.status, `External service "${id}" external.status`, path);
|
|
70
|
+
if (external.discover !== undefined) {
|
|
71
|
+
if (!Array.isArray(external.discover))
|
|
72
|
+
throw new Error(`External service "${id}" external.discover must be an array in ${path}`);
|
|
73
|
+
for (const mapping of external.discover) {
|
|
74
|
+
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) {
|
|
75
|
+
throw new Error(`External service "${id}" discover entries must be objects in ${path}`);
|
|
76
|
+
}
|
|
77
|
+
assertKeys(mapping, ['as', 'source', 'jsonPath', 'pattern'], `External service "${id}" discover entry`, path);
|
|
78
|
+
if (!mapping.as || typeof mapping.as !== 'string') {
|
|
79
|
+
throw new Error(`External service "${id}" discover entry must define string "as" in ${path}`);
|
|
80
|
+
}
|
|
81
|
+
const hasJson = typeof mapping.jsonPath === 'string';
|
|
82
|
+
const hasPattern = typeof mapping.pattern === 'string';
|
|
83
|
+
if (hasJson === hasPattern) {
|
|
84
|
+
throw new Error(`External service "${id}" discover "${mapping.as}" must set exactly one of jsonPath/pattern in ${path}`);
|
|
85
|
+
}
|
|
86
|
+
if (hasPattern)
|
|
87
|
+
assertValidRegex(mapping.pattern, `External service "${id}" discover "${mapping.as}" pattern`, path);
|
|
88
|
+
if (mapping.source !== undefined && mapping.source !== 'status' && mapping.source !== 'up') {
|
|
89
|
+
throw new Error(`External service "${id}" discover "${mapping.as}" source must be "status" or "up" in ${path}`);
|
|
90
|
+
}
|
|
91
|
+
if (mapping.source === 'status' && external.status === undefined && hasJson) {
|
|
92
|
+
// jsonPath against status requires a status command to produce the JSON.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (external.status === undefined && external.discover.some((m) => (m.source ?? 'status') === 'status')) {
|
|
96
|
+
throw new Error(`External service "${id}" has discover entries reading "status" but no external.status command in ${path}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (external.readyWhen !== undefined) {
|
|
100
|
+
assertReadyWhen(external.readyWhen, `External service "${id}" external.readyWhen`, path);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function assertLegacyWorldConfig(value, path) {
|
|
104
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
105
|
+
throw new Error(`World config must be an object: ${path}`);
|
|
106
|
+
}
|
|
107
|
+
const config = value;
|
|
108
|
+
if (!config.id || typeof config.id !== 'string') {
|
|
109
|
+
throw new Error(`World config must define string id: ${path}`);
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(config.services)) {
|
|
112
|
+
throw new Error(`World config must define services[]: ${path}`);
|
|
113
|
+
}
|
|
114
|
+
if (config.network !== undefined)
|
|
115
|
+
config.network = normalizeWorldNetwork(config.network);
|
|
116
|
+
if (config.description !== undefined && typeof config.description !== 'string')
|
|
117
|
+
throw new Error(`World config description must be a string in ${path}`);
|
|
118
|
+
if (config.env !== undefined && (!record(config.env) || Object.values(config.env).some((entry) => typeof entry !== 'string')))
|
|
119
|
+
throw new Error(`World config env must be a string→string object in ${path}`);
|
|
120
|
+
if (config.env?.VOLTER_WORLD_NETWORK_POLICY !== undefined)
|
|
121
|
+
throw new Error(`World network permission belongs in runtime.network, not environment values in ${path}`);
|
|
122
|
+
for (const key of ['actors', 'fixtures'])
|
|
123
|
+
if (config[key] !== undefined && !record(config[key]))
|
|
124
|
+
throw new Error(`World config ${key} must be an object in ${path}`);
|
|
125
|
+
if (config.bare !== undefined && (typeof config.bare !== 'object' || typeof config.bare.name !== 'string' || !/^[a-z0-9_-]+\/[a-z0-9_-]+$/i.test(config.bare.name))) {
|
|
126
|
+
throw new Error(`World config bare.name must be <org>/<world> in ${path}`);
|
|
127
|
+
}
|
|
128
|
+
if (config.remotes !== undefined) {
|
|
129
|
+
if (typeof config.remotes !== 'object' || Array.isArray(config.remotes))
|
|
130
|
+
throw new Error(`World config remotes must be an object of name → url|path in ${path}`);
|
|
131
|
+
for (const [name, target] of Object.entries(config.remotes))
|
|
132
|
+
if (typeof target !== 'string' || target === '' || !/^[A-Za-z0-9._-]+$/.test(name))
|
|
133
|
+
throw new Error(`World config remote "${name}" must be a non-empty url or path in ${path}`);
|
|
134
|
+
}
|
|
135
|
+
if (config.isolation !== undefined && config.isolation !== 'process' && config.isolation !== 'colocated' && config.isolation !== 'worker') {
|
|
136
|
+
throw new Error(`World config isolation must be "process", "colocated", or "worker" in ${path}`);
|
|
137
|
+
}
|
|
138
|
+
if (config.catalog !== undefined) {
|
|
139
|
+
const c = config.catalog;
|
|
140
|
+
if (typeof c !== 'object' || c === null || typeof c.sha !== 'string' || (c.protocol !== undefined && !(typeof c.protocol === 'string' && /^\d+$/.test(c.protocol)))) {
|
|
141
|
+
throw new Error(`World config catalog must be { sha: string, protocol?: "<major>" } (the birth stamp init writes) in ${path}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (config.resources !== undefined) {
|
|
145
|
+
if (!config.resources || typeof config.resources !== 'object' || Array.isArray(config.resources)) {
|
|
146
|
+
throw new Error(`World config resources must be an object in ${path}`);
|
|
147
|
+
}
|
|
148
|
+
for (const key of ['memoryMiB', 'writableStorageMiB']) {
|
|
149
|
+
const amount = config.resources[key];
|
|
150
|
+
if (!Number.isInteger(amount) || amount <= 0) {
|
|
151
|
+
throw new Error(`World config resources.${key} must be a positive integer in ${path}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (config.stripEnv !== undefined && (!Array.isArray(config.stripEnv) || config.stripEnv.some((s) => typeof s !== 'string' || (s !== '*' && !/^[A-Za-z_][A-Za-z0-9_]*\*?$/.test(s))))) {
|
|
156
|
+
throw new Error(`World config stripEnv must be a list of variable names, prefixes ending in *, or * for all inherited variables in ${path}`);
|
|
157
|
+
}
|
|
158
|
+
const ids = new Set();
|
|
159
|
+
for (const service of config.services) {
|
|
160
|
+
if (!service || typeof service !== 'object')
|
|
161
|
+
throw new Error(`Invalid service in ${path}`);
|
|
162
|
+
if (!service.id || typeof service.id !== 'string')
|
|
163
|
+
throw new Error(`Service must define string id in ${path}`);
|
|
164
|
+
const exports = [service.injectEnv, ...Object.keys(service.injectEnvTemplates ?? {}), ...Object.keys(service.cliRedirect ?? {}), ...(Array.isArray(service.external?.discover) ? service.external.discover.map(entry => entry?.as) : [])];
|
|
165
|
+
if (service.env?.VOLTER_WORLD_NETWORK_POLICY !== undefined || exports.includes('VOLTER_WORLD_NETWORK_POLICY'))
|
|
166
|
+
throw new Error(`Service ${service.id} cannot replace World network permission in ${path}`);
|
|
167
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(service.id))
|
|
168
|
+
throw new Error(`Invalid service id "${service.id}" in ${path}`);
|
|
169
|
+
if (ids.has(service.id))
|
|
170
|
+
throw new Error(`Duplicate service id "${service.id}" in ${path}`);
|
|
171
|
+
ids.add(service.id);
|
|
172
|
+
if (service.type !== undefined && service.type !== 'twin' && service.type !== 'process' && service.type !== 'external') {
|
|
173
|
+
throw new Error(`Service "${service.id}" type must be "twin", "process", or "external" in ${path}`);
|
|
174
|
+
}
|
|
175
|
+
if (service.command !== undefined && typeof service.command !== 'string')
|
|
176
|
+
throw new Error(`Service "${service.id}" command must be a string in ${path}`);
|
|
177
|
+
if (service.args !== undefined && (!Array.isArray(service.args) || service.args.some((arg) => typeof arg !== 'string')))
|
|
178
|
+
throw new Error(`Service "${service.id}" args must be a string array in ${path}`);
|
|
179
|
+
if (service.cwd !== undefined && typeof service.cwd !== 'string')
|
|
180
|
+
throw new Error(`Service "${service.id}" cwd must be a string in ${path}`);
|
|
181
|
+
if (service.controlPlane !== undefined && typeof service.controlPlane !== 'boolean')
|
|
182
|
+
throw new Error(`Service "${service.id}" controlPlane must be a boolean in ${path}`);
|
|
183
|
+
for (const key of ['rootArg', 'portArg'])
|
|
184
|
+
if (service[key] !== undefined && service[key] !== false && typeof service[key] !== 'string')
|
|
185
|
+
throw new Error(`Service "${service.id}" ${key} must be a string or false in ${path}`);
|
|
186
|
+
if (service.port !== undefined && service.port !== 'auto' && !(Number.isInteger(service.port) && service.port > 0 && service.port <= 65535))
|
|
187
|
+
throw new Error(`Service "${service.id}" port must be "auto" or an integer from 1 to 65535 in ${path}`);
|
|
188
|
+
if (service.type === 'external') {
|
|
189
|
+
assertExternalServiceConfig(service, path);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
if (service.package !== undefined && !(typeof service.package === 'string' && /^@volter\/twin-[a-z0-9][a-z0-9-]*$/.test(service.package))) {
|
|
193
|
+
throw new Error(`Service "${service.id}" package must name a twin package (@volter/twin-<vendor>) in ${path}`);
|
|
194
|
+
}
|
|
195
|
+
if (service.package === undefined && (!service.command || typeof service.command !== 'string')) {
|
|
196
|
+
throw new Error(`Service "${service.id}" must define command (or package) in ${path}`);
|
|
197
|
+
}
|
|
198
|
+
if (service.ready !== undefined && service.ready !== 'tcp') {
|
|
199
|
+
assertReadyWhen(service.ready, `Service "${service.id}" ready`, path);
|
|
200
|
+
}
|
|
201
|
+
if (service.preload !== undefined) {
|
|
202
|
+
if (!Array.isArray(service.preload) || service.preload.some((p) => typeof p !== 'string')) {
|
|
203
|
+
throw new Error(`Service "${service.id}" preload must be a string array in ${path}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (typeof service.port === 'number') {
|
|
207
|
+
if (!service.portReason || typeof service.portReason !== 'string') {
|
|
208
|
+
throw new Error(`Service "${service.id}" sets a literal "port" — this requires a "portReason" string recording why it can't be 'auto' in ${path}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (service.portReason !== undefined) {
|
|
212
|
+
throw new Error(`Service "${service.id}" sets "portReason" without a literal numeric "port" in ${path}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (service.root !== undefined) {
|
|
216
|
+
const r = service.root;
|
|
217
|
+
if (!record(r))
|
|
218
|
+
throw new Error(`Service "${service.id}" root must be an object in ${path}`);
|
|
219
|
+
assertKeys(r, ['url', 'deploy', 'refresh', 'scope'], `Service "${service.id}" root`, path);
|
|
220
|
+
if (typeof r.url !== 'string' || r.url === '')
|
|
221
|
+
throw new Error(`Service "${service.id}" root.url must be a non-empty string in ${path}`);
|
|
222
|
+
if (r.deploy !== undefined && r.deploy !== 'auto' && r.deploy !== 'gated' && r.deploy !== 'hold')
|
|
223
|
+
throw new Error(`Service "${service.id}" root.deploy must be "auto", "gated" or "hold" in ${path}`);
|
|
224
|
+
if (r.scope !== undefined && typeof r.scope !== 'string')
|
|
225
|
+
throw new Error(`Service "${service.id}" root.scope must be a string in ${path}`);
|
|
226
|
+
if (r.refresh !== undefined) {
|
|
227
|
+
if (!record(r.refresh))
|
|
228
|
+
throw new Error(`Service "${service.id}" root.refresh must be an object in ${path}`);
|
|
229
|
+
assertKeys(r.refresh, ['every', 'webhook'], `Service "${service.id}" root.refresh`, path);
|
|
230
|
+
if (r.refresh.every !== undefined && typeof r.refresh.every !== 'string')
|
|
231
|
+
throw new Error(`Service "${service.id}" root.refresh.every must be a string in ${path}`);
|
|
232
|
+
if (r.refresh.webhook !== undefined && typeof r.refresh.webhook !== 'boolean')
|
|
233
|
+
throw new Error(`Service "${service.id}" root.refresh.webhook must be a boolean in ${path}`);
|
|
234
|
+
}
|
|
235
|
+
if (r.deploy === undefined)
|
|
236
|
+
service.root.deploy = 'gated';
|
|
237
|
+
}
|
|
238
|
+
if (service.colocate !== undefined) {
|
|
239
|
+
if (service.type === 'external') {
|
|
240
|
+
throw new Error(`External service "${service.id}" must not set colocate (its lifecycle is delegated) in ${path}`);
|
|
241
|
+
}
|
|
242
|
+
const colocate = service.colocate;
|
|
243
|
+
if (record(colocate))
|
|
244
|
+
assertKeys(colocate, ['module', 'export', 'scenarioPath'], `Service "${service.id}" colocate`, path);
|
|
245
|
+
if (colocate?.scenarioPath !== undefined && (typeof colocate.scenarioPath !== 'string' || colocate.scenarioPath === '')) {
|
|
246
|
+
throw new Error(`Service "${service.id}" colocate.scenarioPath must be a non-empty string in ${path}`);
|
|
247
|
+
}
|
|
248
|
+
if (!colocate || typeof colocate !== 'object' || Array.isArray(colocate)
|
|
249
|
+
|| (service.package === undefined ? (!colocate.module || typeof colocate.module !== 'string') : (colocate.module !== undefined && (typeof colocate.module !== 'string' || colocate.module === '')))
|
|
250
|
+
|| !colocate.export || typeof colocate.export !== 'string') {
|
|
251
|
+
throw new Error(`Service "${service.id}" colocate must be { module, export } (non-empty strings; module optional with package) in ${path}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (service.version !== undefined && !(typeof service.version === 'string' && /^\^?\d+\.\d+(\.\d+)?$/.test(service.version)))
|
|
255
|
+
throw new Error(`Service "${service.id}" version must pin the twin package as "major.minor.patch" or "^major.minor" in ${path}`);
|
|
256
|
+
if (service.env !== undefined && (!record(service.env) || Object.values(service.env).some((entry) => typeof entry !== 'string')))
|
|
257
|
+
throw new Error(`Service "${service.id}" env must be a string→string object in ${path}`);
|
|
258
|
+
if (service.injectEnv !== undefined && typeof service.injectEnv !== 'string') {
|
|
259
|
+
throw new Error(`Service "${service.id}" injectEnv must be a string in ${path}`);
|
|
260
|
+
}
|
|
261
|
+
if (service.injectEnvTemplates !== undefined) {
|
|
262
|
+
if (typeof service.injectEnvTemplates !== 'object' || service.injectEnvTemplates === null || Array.isArray(service.injectEnvTemplates)
|
|
263
|
+
|| Object.values(service.injectEnvTemplates).some((v) => typeof v !== 'string')) {
|
|
264
|
+
throw new Error(`Service "${service.id}" injectEnvTemplates must be a string→string object in ${path}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (service.cliRedirect !== undefined) {
|
|
268
|
+
if (typeof service.cliRedirect !== 'object' || service.cliRedirect === null || Array.isArray(service.cliRedirect)
|
|
269
|
+
|| Object.values(service.cliRedirect).some((v) => typeof v !== 'string')) {
|
|
270
|
+
throw new Error(`Service "${service.id}" cliRedirect must be a string→string object in ${path}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (config.share !== undefined) {
|
|
275
|
+
if (!config.share || typeof config.share !== 'object' || Array.isArray(config.share)) {
|
|
276
|
+
throw new Error(`World config share must be an object in ${path}`);
|
|
277
|
+
}
|
|
278
|
+
const share = config.share;
|
|
279
|
+
assertKeys(config.share, ['provider', 'command', 'args', 'ephemeral', 'services'], 'World config share', path);
|
|
280
|
+
if (share.provider !== undefined && share.provider !== 'cloudflare-quick' && share.provider !== 'command') {
|
|
281
|
+
throw new Error(`World config share.provider must be "cloudflare-quick" or "command" in ${path}`);
|
|
282
|
+
}
|
|
283
|
+
if (share.provider === 'command' && (typeof share.command !== 'string' || !share.command)) {
|
|
284
|
+
throw new Error(`World config share.provider "command" requires a non-empty share.command in ${path}`);
|
|
285
|
+
}
|
|
286
|
+
if (share.command !== undefined) {
|
|
287
|
+
if (typeof share.command !== 'string' || !share.command) {
|
|
288
|
+
throw new Error(`World config share.command must be a non-empty string in ${path}`);
|
|
289
|
+
}
|
|
290
|
+
if (share.provider !== undefined && share.provider !== 'command') {
|
|
291
|
+
throw new Error(`World config share.command requires share.provider to be omitted or "command" in ${path}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (share.args !== undefined) {
|
|
295
|
+
if (share.command === undefined) {
|
|
296
|
+
throw new Error(`World config share.args is only allowed when share.command is set in ${path}`);
|
|
297
|
+
}
|
|
298
|
+
if (!Array.isArray(share.args) || share.args.some((arg) => typeof arg !== 'string')) {
|
|
299
|
+
throw new Error(`World config share.args must be a string array in ${path}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (share.ephemeral !== undefined && typeof share.ephemeral !== 'boolean') {
|
|
303
|
+
throw new Error(`World config share.ephemeral must be a boolean in ${path}`);
|
|
304
|
+
}
|
|
305
|
+
if (!Array.isArray(share.services))
|
|
306
|
+
throw new Error(`World config share.services must be an array in ${path}`);
|
|
307
|
+
for (const target of share.services) {
|
|
308
|
+
if (!target || typeof target !== 'object' || Array.isArray(target))
|
|
309
|
+
throw new Error(`Invalid share service in ${path}`);
|
|
310
|
+
assertKeys(target, ['id', 'verifyPath'], 'Share service', path);
|
|
311
|
+
if (!target.id || typeof target.id !== 'string')
|
|
312
|
+
throw new Error(`Share service must define string id in ${path}`);
|
|
313
|
+
if (!ids.has(target.id))
|
|
314
|
+
throw new Error(`Share service "${target.id}" does not match a configured service in ${path}`);
|
|
315
|
+
if (target.verifyPath !== undefined && target.verifyPath !== false && typeof target.verifyPath !== 'string') {
|
|
316
|
+
throw new Error(`Share service "${target.id}" verifyPath must be a string or false in ${path}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return config;
|
|
321
|
+
}
|
|
322
|
+
function record(value) {
|
|
323
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
324
|
+
}
|
|
325
|
+
function assertKeys(value, allowed, what, path, allowComments = false) {
|
|
326
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key) && !(allowComments && key.startsWith('//')));
|
|
327
|
+
if (unexpected.length)
|
|
328
|
+
throw new Error(`${what} has unknown field(s) ${unexpected.join(', ')} in ${path}`);
|
|
329
|
+
}
|
|
330
|
+
export const DEFAULT_WORLD_SELECTION = {
|
|
331
|
+
include: [{ usage: 'application' }],
|
|
332
|
+
exclude: [],
|
|
333
|
+
};
|
|
334
|
+
export const LEGACY_WORLD_SELECTION = {
|
|
335
|
+
include: WORLD_USAGES.map((usage) => ({ usage })),
|
|
336
|
+
exclude: [],
|
|
337
|
+
};
|
|
338
|
+
export function assertWorldSelection(value, path) {
|
|
339
|
+
if (!record(value))
|
|
340
|
+
throw new Error(`discovery.selection must be an object in ${path}`);
|
|
341
|
+
assertKeys(value, ['include', 'exclude'], 'discovery.selection', path);
|
|
342
|
+
for (const list of ['include', 'exclude']) {
|
|
343
|
+
if (!Array.isArray(value[list]))
|
|
344
|
+
throw new Error(`discovery.selection.${list} must be an array in ${path}`);
|
|
345
|
+
for (const selector of value[list]) {
|
|
346
|
+
if (!record(selector))
|
|
347
|
+
throw new Error(`discovery.selection.${list} entries must be objects in ${path}`);
|
|
348
|
+
assertKeys(selector, ['usage', 'vendor'], `discovery.selection.${list} selector`, path);
|
|
349
|
+
if (selector.usage === undefined && selector.vendor === undefined)
|
|
350
|
+
throw new Error(`discovery.selection.${list} selectors must not be empty in ${path}`);
|
|
351
|
+
if (selector.usage !== undefined && !WORLD_USAGES.includes(selector.usage))
|
|
352
|
+
throw new Error(`Invalid discovery usage "${String(selector.usage)}" in ${path}`);
|
|
353
|
+
if (selector.vendor !== undefined && (typeof selector.vendor !== 'string' || selector.vendor.trim() === ''))
|
|
354
|
+
throw new Error(`discovery selector vendor must be a non-empty string in ${path}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function serviceV2ToLegacy(value, path) {
|
|
359
|
+
if (!record(value))
|
|
360
|
+
throw new Error(`World service must be an object in ${path}`);
|
|
361
|
+
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root'], `Service ${String(value.id ?? '')}`, path, true);
|
|
362
|
+
const source = value.source === undefined ? {} : value.source;
|
|
363
|
+
const execution = value.execution === undefined ? {} : value.execution;
|
|
364
|
+
const endpoint = value.endpoint === undefined ? {} : value.endpoint;
|
|
365
|
+
const bindings = value.bindings === undefined ? {} : value.bindings;
|
|
366
|
+
if (!record(source) || !record(execution) || !record(endpoint) || !record(bindings))
|
|
367
|
+
throw new Error(`Service "${String(value.id ?? '')}" sections must be objects in ${path}`);
|
|
368
|
+
assertKeys(source, ['package', 'version'], 'service.source', path);
|
|
369
|
+
assertKeys(execution, ['process', 'colocate', 'lifecycle', 'cwd', 'environment', 'preload', 'controlPlane'], 'service.execution', path);
|
|
370
|
+
assertKeys(endpoint, ['port', 'portReason', 'ready'], 'service.endpoint', path);
|
|
371
|
+
assertKeys(bindings, ['injectEnv', 'injectEnvTemplates', 'cliRedirect', 'discover'], 'service.bindings', path);
|
|
372
|
+
const process = execution.process === undefined ? {} : execution.process;
|
|
373
|
+
const environment = execution.environment === undefined ? {} : execution.environment;
|
|
374
|
+
if (!record(process) || !record(environment))
|
|
375
|
+
throw new Error(`Service execution process/environment must be objects in ${path}`);
|
|
376
|
+
assertKeys(process, ['command', 'args', 'rootArg', 'portArg'], 'service.execution.process', path);
|
|
377
|
+
assertKeys(environment, ['values'], 'service.execution.environment', path);
|
|
378
|
+
if (typeof value.id !== 'string' || value.id === '')
|
|
379
|
+
throw new Error(`World service must define string id in ${path}`);
|
|
380
|
+
if (value.description !== undefined && typeof value.description !== 'string')
|
|
381
|
+
throw new Error(`Service "${value.id}" description must be a string in ${path}`);
|
|
382
|
+
if (value.description !== undefined && value['//'] !== undefined)
|
|
383
|
+
throw new Error(`Service "${value.id}" must not set both description and // in ${path}`);
|
|
384
|
+
if (execution.cwd !== undefined && typeof execution.cwd !== 'string')
|
|
385
|
+
throw new Error(`Service "${value.id}" execution.cwd must be a string in ${path}`);
|
|
386
|
+
if (execution.controlPlane !== undefined && typeof execution.controlPlane !== 'boolean')
|
|
387
|
+
throw new Error(`Service "${value.id}" execution.controlPlane must be a boolean in ${path}`);
|
|
388
|
+
if (execution.colocate !== undefined) {
|
|
389
|
+
if (!record(execution.colocate))
|
|
390
|
+
throw new Error(`Service "${value.id}" execution.colocate must be an object in ${path}`);
|
|
391
|
+
assertKeys(execution.colocate, ['module', 'export', 'scenarioPath'], `Service "${value.id}" execution.colocate`, path);
|
|
392
|
+
}
|
|
393
|
+
if (execution.preload !== undefined && (!Array.isArray(execution.preload) || execution.preload.some((entry) => typeof entry !== 'string')))
|
|
394
|
+
throw new Error(`Service "${value.id}" execution.preload must be a string array in ${path}`);
|
|
395
|
+
if (execution.lifecycle !== undefined) {
|
|
396
|
+
if (!record(execution.lifecycle))
|
|
397
|
+
throw new Error(`Service "${value.id}" execution.lifecycle must be an object in ${path}`);
|
|
398
|
+
assertKeys(execution.lifecycle, ['up', 'status', 'down', 'readyWhen'], `Service "${value.id}" execution.lifecycle`, path);
|
|
399
|
+
}
|
|
400
|
+
if (bindings.discover !== undefined) {
|
|
401
|
+
if (!Array.isArray(bindings.discover))
|
|
402
|
+
throw new Error(`Service "${value.id}" bindings.discover must be an array in ${path}`);
|
|
403
|
+
for (const mapping of bindings.discover) {
|
|
404
|
+
if (!record(mapping))
|
|
405
|
+
throw new Error(`Service "${value.id}" bindings.discover entries must be objects in ${path}`);
|
|
406
|
+
assertKeys(mapping, ['as', 'source', 'jsonPath', 'pattern'], `Service "${value.id}" bindings.discover entry`, path);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const type = value.type;
|
|
410
|
+
if (type !== 'twin' && type !== 'process' && type !== 'external')
|
|
411
|
+
throw new Error(`Service "${String(value.id ?? '')}" must explicitly set type in ${path}`);
|
|
412
|
+
const lifecycle = execution.lifecycle;
|
|
413
|
+
const external = lifecycle === undefined ? undefined : {
|
|
414
|
+
...lifecycle,
|
|
415
|
+
...(bindings.discover === undefined ? {} : { discover: bindings.discover }),
|
|
416
|
+
};
|
|
417
|
+
const legacy = {
|
|
418
|
+
...Object.fromEntries(Object.entries(value).filter(([key]) => key.startsWith('//'))),
|
|
419
|
+
...(typeof value.description === 'string' ? { '//': value.description } : {}),
|
|
420
|
+
id: value.id,
|
|
421
|
+
type,
|
|
422
|
+
...(source.package === undefined ? {} : { package: source.package }),
|
|
423
|
+
...(source.version === undefined ? {} : { version: source.version }),
|
|
424
|
+
...(process.command === undefined ? {} : { command: process.command }),
|
|
425
|
+
...(process.args === undefined ? {} : { args: process.args }),
|
|
426
|
+
...(process.rootArg === undefined ? {} : { rootArg: process.rootArg }),
|
|
427
|
+
...(process.portArg === undefined ? {} : { portArg: process.portArg }),
|
|
428
|
+
...(execution.colocate === undefined ? {} : { colocate: execution.colocate }),
|
|
429
|
+
...(execution.cwd === undefined ? {} : { cwd: execution.cwd }),
|
|
430
|
+
...(environment.values === undefined ? {} : { env: environment.values }),
|
|
431
|
+
...(execution.preload === undefined ? {} : { preload: execution.preload }),
|
|
432
|
+
...(execution.controlPlane === undefined ? {} : { controlPlane: execution.controlPlane }),
|
|
433
|
+
...(endpoint.port === undefined ? {} : { port: endpoint.port }),
|
|
434
|
+
...(endpoint.portReason === undefined ? {} : { portReason: endpoint.portReason }),
|
|
435
|
+
...(endpoint.ready === undefined ? {} : { ready: endpoint.ready }),
|
|
436
|
+
...(bindings.injectEnv === undefined ? {} : { injectEnv: bindings.injectEnv }),
|
|
437
|
+
...(bindings.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: bindings.injectEnvTemplates }),
|
|
438
|
+
...(bindings.cliRedirect === undefined ? {} : { cliRedirect: bindings.cliRedirect }),
|
|
439
|
+
...(value.root === undefined ? {} : { root: value.root }),
|
|
440
|
+
...(external === undefined ? {} : { external }),
|
|
441
|
+
};
|
|
442
|
+
if (type !== 'external' && bindings.discover !== undefined)
|
|
443
|
+
throw new Error(`Service "${legacy.id}" bindings.discover is only valid for external services in ${path}`);
|
|
444
|
+
if (type === 'twin' && ((legacy.package === undefined) === (legacy.command === undefined) || lifecycle !== undefined))
|
|
445
|
+
throw new Error(`Twin service "${legacy.id}" requires exactly one of source.package or execution.process.command and forbids lifecycle in ${path}`);
|
|
446
|
+
if (type === 'process' && (legacy.command === undefined || value.source !== undefined || execution.colocate !== undefined || lifecycle !== undefined || legacy.root !== undefined))
|
|
447
|
+
throw new Error(`Process service "${legacy.id}" has an invalid source/execution/root combination in ${path}`);
|
|
448
|
+
if (type === 'external' && (legacy.external === undefined || value.source !== undefined || execution.process !== undefined || execution.colocate !== undefined || value.endpoint !== undefined || execution.preload !== undefined || legacy.root !== undefined || bindings.injectEnv !== undefined || bindings.injectEnvTemplates !== undefined || bindings.cliRedirect !== undefined))
|
|
449
|
+
throw new Error(`External service "${legacy.id}" has an invalid source/execution/endpoint/bindings/root combination in ${path}`);
|
|
450
|
+
return legacy;
|
|
451
|
+
}
|
|
452
|
+
function normalizeV2(value, path) {
|
|
453
|
+
assertKeys(value, ['schemaVersion', 'metadata', 'discovery', 'services', 'runtime', 'serving', 'remotes', 'scenario', 'provenance'], 'World manifest', path, true);
|
|
454
|
+
for (const name of ['metadata', 'discovery', 'runtime', 'serving', 'scenario', 'provenance'])
|
|
455
|
+
if (value[name] !== undefined && !record(value[name]))
|
|
456
|
+
throw new Error(`${name} must be an object in ${path}`);
|
|
457
|
+
const metadata = value.metadata;
|
|
458
|
+
if (!metadata)
|
|
459
|
+
throw new Error(`World manifest must define metadata in ${path}`);
|
|
460
|
+
assertKeys(metadata, ['id', 'description'], 'metadata', path);
|
|
461
|
+
if (typeof metadata.id !== 'string' || metadata.id === '')
|
|
462
|
+
throw new Error(`metadata.id must be a non-empty string in ${path}`);
|
|
463
|
+
if (metadata.description !== undefined && typeof metadata.description !== 'string')
|
|
464
|
+
throw new Error(`metadata.description must be a string in ${path}`);
|
|
465
|
+
const discovery = (value.discovery ?? {});
|
|
466
|
+
const runtime = (value.runtime ?? {});
|
|
467
|
+
const serving = (value.serving ?? {});
|
|
468
|
+
const scenario = (value.scenario ?? {});
|
|
469
|
+
const provenance = (value.provenance ?? {});
|
|
470
|
+
assertKeys(discovery, ['selection'], 'discovery', path);
|
|
471
|
+
assertKeys(runtime, ['isolation', 'environment', 'network'], 'runtime', path);
|
|
472
|
+
assertKeys(serving, ['mode', 'name', 'share'], 'serving', path);
|
|
473
|
+
assertKeys(scenario, ['actors', 'fixtures'], 'scenario', path);
|
|
474
|
+
assertKeys(provenance, ['catalog'], 'provenance', path);
|
|
475
|
+
const environment = runtime.environment ?? {};
|
|
476
|
+
if (!record(environment))
|
|
477
|
+
throw new Error(`runtime.environment must be an object in ${path}`);
|
|
478
|
+
assertKeys(environment, ['values', 'strip'], 'runtime.environment', path);
|
|
479
|
+
if (environment.values !== undefined && (!record(environment.values) || Object.values(environment.values).some((entry) => typeof entry !== 'string')))
|
|
480
|
+
throw new Error(`runtime.environment.values must be a string→string object in ${path}`);
|
|
481
|
+
if (scenario.actors !== undefined && !record(scenario.actors))
|
|
482
|
+
throw new Error(`scenario.actors must be an object in ${path}`);
|
|
483
|
+
if (scenario.fixtures !== undefined && !record(scenario.fixtures))
|
|
484
|
+
throw new Error(`scenario.fixtures must be an object in ${path}`);
|
|
485
|
+
const mode = serving.mode ?? 'app';
|
|
486
|
+
if (mode !== 'app' && mode !== 'bare')
|
|
487
|
+
throw new Error(`serving.mode must be "app" or "bare" in ${path}`);
|
|
488
|
+
if (mode === 'bare' && typeof serving.name !== 'string')
|
|
489
|
+
throw new Error(`Bare serving requires serving.name in ${path}`);
|
|
490
|
+
if (mode === 'app' && serving.name !== undefined)
|
|
491
|
+
throw new Error(`App serving must omit serving.name in ${path}`);
|
|
492
|
+
const selection = discovery.selection ?? DEFAULT_WORLD_SELECTION;
|
|
493
|
+
assertWorldSelection(selection, path);
|
|
494
|
+
if (!Array.isArray(value.services))
|
|
495
|
+
throw new Error(`World manifest must define services[] in ${path}`);
|
|
496
|
+
const normalized = {
|
|
497
|
+
...Object.fromEntries(Object.entries(value).filter(([key]) => key.startsWith('//'))),
|
|
498
|
+
id: metadata.id,
|
|
499
|
+
...(metadata.description === undefined ? {} : { description: metadata.description }),
|
|
500
|
+
manifestVersion: 2,
|
|
501
|
+
selection,
|
|
502
|
+
services: value.services.map((service) => serviceV2ToLegacy(service, path)),
|
|
503
|
+
...(runtime.isolation === undefined ? {} : { isolation: runtime.isolation }),
|
|
504
|
+
...(runtime.network === undefined ? {} : { network: normalizeWorldNetwork(runtime.network) }),
|
|
505
|
+
...(environment.values === undefined ? {} : { env: environment.values }),
|
|
506
|
+
...(environment.strip === undefined ? {} : { stripEnv: environment.strip }),
|
|
507
|
+
...(mode === 'bare' ? { bare: { name: serving.name } } : {}),
|
|
508
|
+
...(serving.share === undefined ? {} : { share: serving.share }),
|
|
509
|
+
...(value.remotes === undefined ? {} : { remotes: value.remotes }),
|
|
510
|
+
...(scenario.actors === undefined ? {} : { actors: scenario.actors }),
|
|
511
|
+
...(scenario.fixtures === undefined ? {} : { fixtures: scenario.fixtures }),
|
|
512
|
+
...(provenance.catalog === undefined ? {} : { catalog: provenance.catalog }),
|
|
513
|
+
};
|
|
514
|
+
return { ...assertLegacyWorldConfig(normalized, path), manifestVersion: 2, selection };
|
|
515
|
+
}
|
|
516
|
+
export function assertWorldConfig(value, path) {
|
|
517
|
+
if (record(value) && value.schemaVersion !== undefined) {
|
|
518
|
+
if (value.schemaVersion !== WORLD_SCHEMA_VERSION)
|
|
519
|
+
throw new Error(`Unsupported World schemaVersion ${String(value.schemaVersion)} in ${path}`);
|
|
520
|
+
if ('id' in value || 'env' in value || 'isolation' in value || 'share' in value || 'catalog' in value)
|
|
521
|
+
throw new Error(`World manifest mixes format-1 and format-2 fields in ${path}`);
|
|
522
|
+
return normalizeV2(value, path);
|
|
523
|
+
}
|
|
524
|
+
const legacy = assertLegacyWorldConfig(value, path);
|
|
525
|
+
return { ...legacy, manifestVersion: 1 };
|
|
526
|
+
}
|
|
527
|
+
export function worldSelectionIncludes(selection, vendor, usage) {
|
|
528
|
+
const matches = (selector) => (selector.vendor === undefined || selector.vendor === vendor) && (selector.usage === undefined || selector.usage === usage);
|
|
529
|
+
return selection.include.some(matches) && !selection.exclude.some(matches);
|
|
530
|
+
}
|
|
531
|
+
function serviceToV2(service) {
|
|
532
|
+
const process = {
|
|
533
|
+
...(service.command === undefined ? {} : { command: service.command }),
|
|
534
|
+
...(service.args === undefined ? {} : { args: service.args }),
|
|
535
|
+
...(service.rootArg === undefined ? {} : { rootArg: service.rootArg }),
|
|
536
|
+
...(service.portArg === undefined ? {} : { portArg: service.portArg }),
|
|
537
|
+
};
|
|
538
|
+
const execution = {
|
|
539
|
+
...(Object.keys(process).length ? { process } : {}),
|
|
540
|
+
...(service.colocate === undefined ? {} : { colocate: service.colocate }),
|
|
541
|
+
...(service.external === undefined ? {} : { lifecycle: {
|
|
542
|
+
up: service.external.up,
|
|
543
|
+
...(service.external.status === undefined ? {} : { status: service.external.status }),
|
|
544
|
+
down: service.external.down,
|
|
545
|
+
...(service.external.readyWhen === undefined ? {} : { readyWhen: service.external.readyWhen }),
|
|
546
|
+
} }),
|
|
547
|
+
...(service.cwd === undefined ? {} : { cwd: service.cwd }),
|
|
548
|
+
...(service.env === undefined ? {} : { environment: { values: service.env } }),
|
|
549
|
+
...(service.preload === undefined ? {} : { preload: service.preload }),
|
|
550
|
+
...(service.controlPlane === undefined ? {} : { controlPlane: service.controlPlane }),
|
|
551
|
+
};
|
|
552
|
+
const endpoint = {
|
|
553
|
+
...(service.port === undefined ? {} : { port: service.port }),
|
|
554
|
+
...(service.portReason === undefined ? {} : { portReason: service.portReason }),
|
|
555
|
+
...(service.ready === undefined ? {} : { ready: service.ready }),
|
|
556
|
+
};
|
|
557
|
+
const bindings = {
|
|
558
|
+
...(service.injectEnv === undefined ? {} : { injectEnv: service.injectEnv }),
|
|
559
|
+
...(service.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: service.injectEnvTemplates }),
|
|
560
|
+
...(service.cliRedirect === undefined ? {} : { cliRedirect: service.cliRedirect }),
|
|
561
|
+
...(service.external?.discover === undefined ? {} : { discover: service.external.discover }),
|
|
562
|
+
};
|
|
563
|
+
return {
|
|
564
|
+
...Object.fromEntries(Object.entries(service).filter(([key]) => key.startsWith('//') && key !== '//')),
|
|
565
|
+
...(service['//'] === undefined ? {} : { description: service['//'] }),
|
|
566
|
+
id: service.id,
|
|
567
|
+
type: service.type ?? 'twin',
|
|
568
|
+
...(service.package === undefined && service.version === undefined ? {} : { source: {
|
|
569
|
+
...(service.package === undefined ? {} : { package: service.package }),
|
|
570
|
+
...(service.version === undefined ? {} : { version: service.version }),
|
|
571
|
+
} }),
|
|
572
|
+
...(Object.keys(execution).length ? { execution } : {}),
|
|
573
|
+
...(Object.keys(endpoint).length ? { endpoint } : {}),
|
|
574
|
+
...(Object.keys(bindings).length ? { bindings } : {}),
|
|
575
|
+
...(service.root === undefined ? {} : { root: service.root }),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
/** Serialize normalized intent as the committed format-2 manifest. */
|
|
579
|
+
export function worldConfigDocument(config, selection = config.selection ?? DEFAULT_WORLD_SELECTION) {
|
|
580
|
+
return {
|
|
581
|
+
schemaVersion: WORLD_SCHEMA_VERSION,
|
|
582
|
+
metadata: { id: config.id, ...(config.description === undefined ? {} : { description: config.description }) },
|
|
583
|
+
discovery: { selection },
|
|
584
|
+
...(config.isolation === undefined && config.env === undefined && config.stripEnv === undefined && config.network === undefined ? {} : { runtime: {
|
|
585
|
+
...(config.isolation === undefined ? {} : { isolation: config.isolation }),
|
|
586
|
+
...(config.network === undefined ? {} : { network: normalizeWorldNetwork(config.network) }),
|
|
587
|
+
...(config.env === undefined && config.stripEnv === undefined ? {} : { environment: {
|
|
588
|
+
...(config.env === undefined ? {} : { values: config.env }),
|
|
589
|
+
...(config.stripEnv === undefined ? {} : { strip: config.stripEnv }),
|
|
590
|
+
} }),
|
|
591
|
+
} }),
|
|
592
|
+
services: config.services.map(serviceToV2),
|
|
593
|
+
...(config.bare === undefined && config.share === undefined ? {} : { serving: {
|
|
594
|
+
...(config.bare === undefined ? {} : { mode: 'bare', name: config.bare.name }),
|
|
595
|
+
...(config.share === undefined ? {} : { share: config.share }),
|
|
596
|
+
} }),
|
|
597
|
+
...(config.remotes === undefined ? {} : { remotes: config.remotes }),
|
|
598
|
+
...(config.actors === undefined && config.fixtures === undefined ? {} : { scenario: {
|
|
599
|
+
...(config.actors === undefined ? {} : { actors: config.actors }),
|
|
600
|
+
...(config.fixtures === undefined ? {} : { fixtures: config.fixtures }),
|
|
601
|
+
} }),
|
|
602
|
+
...(config.catalog === undefined ? {} : { provenance: { catalog: config.catalog } }),
|
|
603
|
+
...Object.fromEntries(Object.entries(config).filter(([key]) => key.startsWith('//'))),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type WorldManifest } from './attach.js';
|
|
2
|
+
export declare const MANIFEST_PATH = "/.well-known/volter-world";
|
|
3
|
+
/** The manifest a REMOTE attacher sees: every vendor behind the one advertised
|
|
4
|
+
* door. The serving side keeps routing against its own loopback env. */
|
|
5
|
+
export declare function advertiseWorldManifest(name: string, root: string, advertisedOrigin: string): WorldManifest;
|
|
6
|
+
export interface ManifestServerOptions {
|
|
7
|
+
manifest: () => WorldManifest;
|
|
8
|
+
host?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
/** when set, GET requires `Authorization: Bearer <token>` — the attach token
|
|
11
|
+
* for a shared world (docs/guides/route-a-cli-through-the-world.md). Constant-time-ish compare via length
|
|
12
|
+
* gate + timingSafeEqual to avoid trivial token oracles. */
|
|
13
|
+
token?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ManifestServerHandle {
|
|
16
|
+
port: number;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function startManifestServer(options: ManifestServerOptions): Promise<ManifestServerHandle>;
|
|
20
|
+
/** Fetch a remote world's manifest. Accepts the manifest URL itself or the
|
|
21
|
+
* world's base URL (the well-known path is appended). */
|
|
22
|
+
export declare function fetchRemoteManifest(ref: string, options?: {
|
|
23
|
+
token?: string;
|
|
24
|
+
}): Promise<WorldManifest>;
|
|
25
|
+
/** The env a remote env-attachment exports: vendor twin URLs (the advertised
|
|
26
|
+
* door), suggested fake credentials, CA trust, and the injector preload. */
|
|
27
|
+
export declare function remoteAttachEnv(manifest: WorldManifest, options: {
|
|
28
|
+
caFile?: string;
|
|
29
|
+
injectPath: string;
|
|
30
|
+
}): Record<string, string>;
|