@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,74 @@
|
|
|
1
|
+
export declare function reflectRoutesPath(root: string, name: string): string;
|
|
2
|
+
export declare function readReflectRoutes(path: string): Set<string>;
|
|
3
|
+
export declare function writeReflectRoutes(path: string, routes: Set<string>): void;
|
|
4
|
+
/** Extract the SNI server name from a buffered TLS ClientHello, `undefined` if
|
|
5
|
+
* the buffer is not yet a complete handshake record or carries no SNI. */
|
|
6
|
+
export declare function parseSni(buffer: Buffer): string | undefined;
|
|
7
|
+
export interface ReflectFrontOptions {
|
|
8
|
+
/** live env source — the world's twin map, exactly like the redirect proxy */
|
|
9
|
+
envLoader: () => Record<string, string | undefined>;
|
|
10
|
+
tlsDir: string;
|
|
11
|
+
host?: string;
|
|
12
|
+
port?: number;
|
|
13
|
+
/** additional SNI names to terminate (a served world's ADVERTISED hostname):
|
|
14
|
+
* the per-host handler already routes by Host header, so env-attached
|
|
15
|
+
* traffic arriving with SNI=<advertised> and Host=<vendor> lands in the
|
|
16
|
+
* right twin through the one door (docs/guides/route-a-cli-through-the-world.md, remote worlds). */
|
|
17
|
+
extraHosts?: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface ReflectFrontHandle {
|
|
20
|
+
host: string;
|
|
21
|
+
port: number;
|
|
22
|
+
caCertPath: string;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export declare function startReflectFront(options: ReflectFrontOptions): Promise<ReflectFrontHandle>;
|
|
26
|
+
export interface ReflectResolverOptions {
|
|
27
|
+
/** live route set — re-read per query so a route flip lands immediately */
|
|
28
|
+
routesLoader: () => Set<string>;
|
|
29
|
+
/** the address routed hosts resolve to (the front's reachable IP) */
|
|
30
|
+
targetIp: string;
|
|
31
|
+
upstream?: string;
|
|
32
|
+
/** upstream port — default 53; tests point at a fake upstream on a high port */
|
|
33
|
+
upstreamPort?: number;
|
|
34
|
+
host?: string;
|
|
35
|
+
port?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface ReflectResolverHandle {
|
|
38
|
+
port: number;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export declare function startReflectResolver(options: ReflectResolverOptions): Promise<ReflectResolverHandle>;
|
|
42
|
+
export interface ReflectManifest {
|
|
43
|
+
/** the address routed hosts resolve to — where attachers reach the front (the verb's --target-ip) */
|
|
44
|
+
targetIp: string;
|
|
45
|
+
/** the address attachers reach the resolver at (--resolver-ip; default the target): on some Docker hosts
|
|
46
|
+
* the gateway address answers DNS itself (colima's VM does), so the resolver needs another host address */
|
|
47
|
+
resolverIp: string;
|
|
48
|
+
frontPort: number;
|
|
49
|
+
resolverPort: number;
|
|
50
|
+
caCertPath: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function reflectManifestPath(root: string, name: string): string;
|
|
53
|
+
export declare function writeReflectManifest(root: string, name: string, manifest: ReflectManifest): void;
|
|
54
|
+
export declare function readReflectManifest(root: string, name: string): ReflectManifest | undefined;
|
|
55
|
+
export declare function clearReflectManifest(root: string, name: string): void;
|
|
56
|
+
/** Where the CA is mounted inside an attached container, and the env that makes its clients trust it. */
|
|
57
|
+
export declare const ATTACHED_CA_PATH = "/etc/volter-world/ca.pem";
|
|
58
|
+
export declare const CA_TRUST_ENV: readonly ["SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "NODE_EXTRA_CA_CERTS", "GIT_SSL_CAINFO"];
|
|
59
|
+
/**
|
|
60
|
+
* A docker compose override that attaches every named service through reflect: its DNS is the resolver
|
|
61
|
+
* (so routed vendor hosts resolve to the front), `host.docker.internal` stays resolvable (a service-level
|
|
62
|
+
* `dns` bypasses Docker's own resolver, which is what served that name), the session CA is mounted and
|
|
63
|
+
* named by the trust variables the common clients read. Requires the resolver on port 53: Docker takes a
|
|
64
|
+
* DNS address, never a port. Compose merges this after the consumer's own files, touching nothing else.
|
|
65
|
+
*/
|
|
66
|
+
export declare function composeOverrideForReflect(manifest: ReflectManifest, services: string[], worldName: string): string;
|
|
67
|
+
/** Split `docker [docker flags] compose [compose flags] <subcommand…>`; undefined when it is not a compose command. */
|
|
68
|
+
export declare function splitDockerComposeArgs(command: string[]): {
|
|
69
|
+
head: string[];
|
|
70
|
+
composeFlags: string[];
|
|
71
|
+
tail: string[];
|
|
72
|
+
} | undefined;
|
|
73
|
+
/** The same compose command with the override merged last. */
|
|
74
|
+
export declare function dockerComposeWithOverride(command: string[], overridePath: string): string[];
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { serveHttp } from '@volter/world-core';
|
|
2
|
+
import { websocketRelay } from "./websocket-relay.js";
|
|
3
|
+
import { sessionTrustEnv } from "./ca-trust.js";
|
|
4
|
+
// The reflect attachment (docs/guides/route-a-cli-through-the-world.md): attach an UNMODIFIED consumer —
|
|
5
|
+
// any language, any binary, no proxy env, no injector — by answering routed
|
|
6
|
+
// vendor hosts' DNS with the world's own TLS front. The consumer's ordinary
|
|
7
|
+
// `https://api.stripe.com` call roundtrips into the front, which terminates
|
|
8
|
+
// TLS with the session CA's per-host leaf (exactly the redirect-proxy's
|
|
9
|
+
// machinery) and forwards to the twin. Hosts that are not routed resolve real
|
|
10
|
+
// and never touch the world: passthrough is the default.
|
|
11
|
+
//
|
|
12
|
+
// Two small servers, both world-side ("the door belongs to the world"):
|
|
13
|
+
// • the FRONT — a TCP listener that peeks the TLS ClientHello SNI and pipes
|
|
14
|
+
// the raw bytes to a per-host leaf-cert https server (the same
|
|
15
|
+
// one-server-per-host trick redirect-proxy uses for CONNECT, reused here
|
|
16
|
+
// because Bun does not honor SNICallback).
|
|
17
|
+
// • the RESOLVER — a UDP DNS server ("the ear", pointed at by the attacher's
|
|
18
|
+
// environment, e.g. `docker run --dns`): A-answers routed hosts with the
|
|
19
|
+
// front's address (TTL 0 — a route flip lands on the next lookup),
|
|
20
|
+
// empty-answers AAAA for routed hosts (else real IPv6 wins), and forwards
|
|
21
|
+
// everything else to the real upstream untouched.
|
|
22
|
+
//
|
|
23
|
+
// ROUTES are attachment-scoped state (docs/guides/route-a-cli-through-the-world.md): one host per line in
|
|
24
|
+
// the instance's reflect-routes file, re-read live. The world stays ignorant
|
|
25
|
+
// of who is attached; routes only select interception. v1 routes are exact
|
|
26
|
+
// lowercase hostnames (wildcard families like *.atlassian.net: route the
|
|
27
|
+
// specific hosts the consumer uses).
|
|
28
|
+
//
|
|
29
|
+
// Per CLAUDE.md the runtime stays a lifecycle primitive: `reflect` runs in the
|
|
30
|
+
// foreground and supervises nothing; composition (containers, --dns wiring)
|
|
31
|
+
// lives in the cookbook recipe.
|
|
32
|
+
import dgram from 'node:dgram';
|
|
33
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
34
|
+
import { connect as netConnect, createServer as createNetServer } from 'node:net';
|
|
35
|
+
import { join } from 'node:path';
|
|
36
|
+
import { activeVendorMap, ensureCa, leafCertFor, noTwinMessage, proxyTargetFor, twinUnreachableMessage } from "./redirect-proxy.js";
|
|
37
|
+
import { instanceDir } from "./runtime.js";
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Routes — attachment-scoped, file-backed, re-read live by the resolver.
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
export function reflectRoutesPath(root, name) {
|
|
42
|
+
return join(instanceDir(root, name), 'reflect-routes');
|
|
43
|
+
}
|
|
44
|
+
export function readReflectRoutes(path) {
|
|
45
|
+
if (!existsSync(path))
|
|
46
|
+
return new Set();
|
|
47
|
+
return new Set(readFileSync(path, 'utf8')
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map((line) => line.trim().toLowerCase())
|
|
50
|
+
.filter((line) => line.length > 0 && !line.startsWith('#')));
|
|
51
|
+
}
|
|
52
|
+
export function writeReflectRoutes(path, routes) {
|
|
53
|
+
writeFileSync(path, `${[...routes].sort().join('\n')}\n`);
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// SNI peek — minimal ClientHello parser (records buffered until complete).
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/** Extract the SNI server name from a buffered TLS ClientHello, `undefined` if
|
|
59
|
+
* the buffer is not yet a complete handshake record or carries no SNI. */
|
|
60
|
+
export function parseSni(buffer) {
|
|
61
|
+
if (buffer.length < 5 || buffer[0] !== 0x16)
|
|
62
|
+
return undefined; // not a TLS handshake record
|
|
63
|
+
const recordLength = buffer.readUInt16BE(3);
|
|
64
|
+
if (buffer.length < 5 + recordLength)
|
|
65
|
+
return undefined; // incomplete — keep buffering
|
|
66
|
+
let offset = 5;
|
|
67
|
+
if (buffer[offset] !== 0x01)
|
|
68
|
+
return undefined; // not a ClientHello
|
|
69
|
+
offset += 4; // handshake type + 24-bit length
|
|
70
|
+
offset += 2 + 32; // client version + random
|
|
71
|
+
const sessionIdLength = buffer[offset];
|
|
72
|
+
offset += 1 + sessionIdLength;
|
|
73
|
+
const cipherLength = buffer.readUInt16BE(offset);
|
|
74
|
+
offset += 2 + cipherLength;
|
|
75
|
+
const compressionLength = buffer[offset];
|
|
76
|
+
offset += 1 + compressionLength;
|
|
77
|
+
if (offset + 2 > buffer.length)
|
|
78
|
+
return undefined;
|
|
79
|
+
const extensionsEnd = offset + 2 + buffer.readUInt16BE(offset);
|
|
80
|
+
offset += 2;
|
|
81
|
+
while (offset + 4 <= extensionsEnd && offset + 4 <= buffer.length) {
|
|
82
|
+
const extensionType = buffer.readUInt16BE(offset);
|
|
83
|
+
const extensionLength = buffer.readUInt16BE(offset + 2);
|
|
84
|
+
offset += 4;
|
|
85
|
+
if (extensionType === 0x0000) {
|
|
86
|
+
// server_name: list length (2), entry type (1, 0 = hostname), name length (2), name
|
|
87
|
+
const nameLength = buffer.readUInt16BE(offset + 3);
|
|
88
|
+
return buffer.subarray(offset + 5, offset + 5 + nameLength).toString('ascii').toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
offset += extensionLength;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
export async function startReflectFront(options) {
|
|
95
|
+
const host = options.host ?? '0.0.0.0';
|
|
96
|
+
const ca = ensureCa(options.tlsDir);
|
|
97
|
+
const openSockets = new Set();
|
|
98
|
+
const hostServers = new Map();
|
|
99
|
+
const hostPorts = new Map();
|
|
100
|
+
// One TLS server per vendor host (the leaf cert names it), behind the SNI door. Bun's own server: it
|
|
101
|
+
// terminates TLS, forwards each request to the twin, and relays a websocket upgrade (a bot's gateway,
|
|
102
|
+
// a streaming API) to the twin's websocket — under Bun, node:http's 'upgrade' socket cannot write back
|
|
103
|
+
// to the client, so the relay is message-level, both ways.
|
|
104
|
+
const ensureHostServer = async (vendorHost) => {
|
|
105
|
+
const existing = hostPorts.get(vendorHost);
|
|
106
|
+
if (existing)
|
|
107
|
+
return existing;
|
|
108
|
+
const leaf = leafCertFor(vendorHost, ca);
|
|
109
|
+
const resolveTwin = (req) => {
|
|
110
|
+
const url = new URL(req.url);
|
|
111
|
+
const hostHeader = (req.headers.get('host') ?? vendorHost).split(':')[0] ?? vendorHost;
|
|
112
|
+
const env = options.envLoader();
|
|
113
|
+
const prefixMatch = url.pathname.match(/^\/__vendor\/([a-z0-9-]+)(\/.*)?$/);
|
|
114
|
+
if (prefixMatch) {
|
|
115
|
+
const origin = activeVendorMap(env)[prefixMatch[1]];
|
|
116
|
+
if (origin === undefined)
|
|
117
|
+
return { refuse: new Response('no twin for vendor', { status: 502 }) };
|
|
118
|
+
return { origin, path: `${prefixMatch[2] ?? '/'}${url.search}`, vendor: prefixMatch[1] };
|
|
119
|
+
}
|
|
120
|
+
const twin = proxyTargetFor(hostHeader, env, url.pathname) ?? proxyTargetFor(vendorHost, env, url.pathname);
|
|
121
|
+
if (!twin)
|
|
122
|
+
return { refuse: new Response(noTwinMessage(hostHeader, url.pathname), { status: 502, headers: { 'content-type': 'text/plain; charset=utf-8' } }) };
|
|
123
|
+
return { origin: twin.origin, path: `${url.pathname}${url.search}`, vendor: twin.vendor };
|
|
124
|
+
};
|
|
125
|
+
const hostServer = await serveHttp({
|
|
126
|
+
hostname: '127.0.0.1',
|
|
127
|
+
port: 0,
|
|
128
|
+
tls: { key: leaf.key, cert: leaf.cert },
|
|
129
|
+
idleTimeout: 255,
|
|
130
|
+
async fetch(req) {
|
|
131
|
+
const target = resolveTwin(req);
|
|
132
|
+
if ('refuse' in target)
|
|
133
|
+
return target.refuse;
|
|
134
|
+
let origin;
|
|
135
|
+
try {
|
|
136
|
+
origin = new URL(target.origin);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return new Response('bad twin origin', { status: 502 });
|
|
140
|
+
}
|
|
141
|
+
const headers = new Headers(req.headers);
|
|
142
|
+
headers.delete('proxy-connection');
|
|
143
|
+
headers.delete('accept-encoding');
|
|
144
|
+
// the consumer spoke TLS to the front; a twin that answers with an absolute URL (Slack's Socket Mode link)
|
|
145
|
+
// gives the scheme the consumer can reach again, as a vendor behind a TLS-terminating edge does
|
|
146
|
+
headers.set('x-forwarded-proto', 'https');
|
|
147
|
+
try {
|
|
148
|
+
const upstream = await fetch(`${origin.origin}${target.path}`, { method: req.method, headers, body: req.body, ...(req.body ? { duplex: 'half' } : {}), redirect: 'manual' });
|
|
149
|
+
const out = new Headers(upstream.headers);
|
|
150
|
+
out.delete('content-encoding');
|
|
151
|
+
out.delete('content-length');
|
|
152
|
+
out.delete('transfer-encoding');
|
|
153
|
+
out.delete('connection');
|
|
154
|
+
out.delete('keep-alive');
|
|
155
|
+
return new Response(upstream.body, { status: upstream.status, headers: out });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
let world;
|
|
159
|
+
try {
|
|
160
|
+
world = options.envLoader().VOLTER_WORLD;
|
|
161
|
+
}
|
|
162
|
+
catch { /* the answer still goes out */ }
|
|
163
|
+
return new Response(twinUnreachableMessage(target.vendor, origin.origin, error instanceof Error ? error : new Error(String(error)), world), { status: 502 });
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
upgrade: websocketRelay(req => {
|
|
167
|
+
const target = resolveTwin(req);
|
|
168
|
+
if ('refuse' in target)
|
|
169
|
+
return undefined;
|
|
170
|
+
const origin = new URL(target.origin);
|
|
171
|
+
return `${origin.protocol === 'https:' ? 'wss' : 'ws'}://${origin.host}${target.path}`;
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
const hostPort = Number(hostServer.port);
|
|
175
|
+
hostServers.set(vendorHost, hostServer);
|
|
176
|
+
hostPorts.set(vendorHost, hostPort);
|
|
177
|
+
return hostPort;
|
|
178
|
+
};
|
|
179
|
+
const server = createNetServer((clientSocket) => {
|
|
180
|
+
openSockets.add(clientSocket);
|
|
181
|
+
clientSocket.on('close', () => openSockets.delete(clientSocket));
|
|
182
|
+
clientSocket.on('error', () => clientSocket.destroy());
|
|
183
|
+
let buffered = Buffer.alloc(0);
|
|
184
|
+
const onData = (chunk) => {
|
|
185
|
+
buffered = Buffer.concat([buffered, chunk]);
|
|
186
|
+
const sni = parseSni(buffered);
|
|
187
|
+
if (sni === undefined) {
|
|
188
|
+
if (buffered.length > 65_536)
|
|
189
|
+
clientSocket.destroy(); // not a ClientHello — give up
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
clientSocket.removeListener('data', onData);
|
|
193
|
+
const twin = proxyTargetFor(sni, options.envLoader());
|
|
194
|
+
if (!twin && !(options.extraHosts ?? []).includes(sni)) {
|
|
195
|
+
// only routed hosts (or the advertised name) are pointed here by the
|
|
196
|
+
// resolver/manifest; anything else is refused, never blind-forwarded
|
|
197
|
+
// (the front is a door to twins, not a general proxy)
|
|
198
|
+
clientSocket.destroy();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
ensureHostServer(sni)
|
|
202
|
+
.then((port) => {
|
|
203
|
+
const upstream = netConnect(port, '127.0.0.1');
|
|
204
|
+
openSockets.add(upstream);
|
|
205
|
+
upstream.on('close', () => openSockets.delete(upstream));
|
|
206
|
+
upstream.on('error', () => clientSocket.destroy());
|
|
207
|
+
clientSocket.on('error', () => upstream.destroy());
|
|
208
|
+
upstream.write(buffered);
|
|
209
|
+
upstream.pipe(clientSocket);
|
|
210
|
+
clientSocket.pipe(upstream);
|
|
211
|
+
})
|
|
212
|
+
.catch(() => clientSocket.destroy());
|
|
213
|
+
};
|
|
214
|
+
clientSocket.on('data', onData);
|
|
215
|
+
});
|
|
216
|
+
await new Promise((resolveListen, reject) => {
|
|
217
|
+
server.once('error', reject);
|
|
218
|
+
server.listen(options.port ?? 0, host, () => resolveListen());
|
|
219
|
+
});
|
|
220
|
+
const address = server.address();
|
|
221
|
+
const port = typeof address === 'object' && address ? address.port : (options.port ?? 0);
|
|
222
|
+
return {
|
|
223
|
+
host,
|
|
224
|
+
port,
|
|
225
|
+
caCertPath: ca.caCert,
|
|
226
|
+
async close() {
|
|
227
|
+
for (const socket of openSockets)
|
|
228
|
+
socket.destroy();
|
|
229
|
+
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
230
|
+
for (const hostServer of hostServers.values())
|
|
231
|
+
hostServer.stop(true);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function dnsQuestion(message) {
|
|
236
|
+
if (message.length < 17)
|
|
237
|
+
return undefined;
|
|
238
|
+
const labels = [];
|
|
239
|
+
let offset = 12;
|
|
240
|
+
while (offset < message.length) {
|
|
241
|
+
const length = message[offset];
|
|
242
|
+
if (length === 0) {
|
|
243
|
+
offset += 1;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
labels.push(message.subarray(offset + 1, offset + 1 + length).toString('ascii'));
|
|
247
|
+
offset += 1 + length;
|
|
248
|
+
}
|
|
249
|
+
if (offset + 4 > message.length)
|
|
250
|
+
return undefined;
|
|
251
|
+
return { name: labels.join('.').toLowerCase(), qtype: message.readUInt16BE(offset), questionEnd: offset + 4 };
|
|
252
|
+
}
|
|
253
|
+
function dnsHeader(query, answerCount) {
|
|
254
|
+
const header = Buffer.alloc(12);
|
|
255
|
+
query.copy(header, 0, 0, 2); // ID
|
|
256
|
+
header.writeUInt16BE(0x8180, 2); // response, RD|RA, NOERROR
|
|
257
|
+
header.writeUInt16BE(1, 4); // QDCOUNT
|
|
258
|
+
header.writeUInt16BE(answerCount, 6); // ANCOUNT
|
|
259
|
+
return header;
|
|
260
|
+
}
|
|
261
|
+
function dnsAnswerA(query, questionEnd, ip) {
|
|
262
|
+
const answer = Buffer.alloc(16);
|
|
263
|
+
answer.writeUInt16BE(0xc00c, 0); // name: pointer to the question
|
|
264
|
+
answer.writeUInt16BE(1, 2); // TYPE A
|
|
265
|
+
answer.writeUInt16BE(1, 4); // CLASS IN
|
|
266
|
+
answer.writeUInt32BE(0, 6); // TTL 0 — activation flips on the next lookup
|
|
267
|
+
answer.writeUInt16BE(4, 10);
|
|
268
|
+
const parts = ip.split('.').map(Number);
|
|
269
|
+
for (let index = 0; index < 4; index++)
|
|
270
|
+
answer[12 + index] = parts[index];
|
|
271
|
+
return Buffer.concat([dnsHeader(query, 1), query.subarray(12, questionEnd), answer]);
|
|
272
|
+
}
|
|
273
|
+
function dnsAnswerEmpty(query, questionEnd) {
|
|
274
|
+
return Buffer.concat([dnsHeader(query, 0), query.subarray(12, questionEnd)]);
|
|
275
|
+
}
|
|
276
|
+
export async function startReflectResolver(options) {
|
|
277
|
+
const upstream = options.upstream ?? '8.8.8.8';
|
|
278
|
+
const server = dgram.createSocket('udp4');
|
|
279
|
+
server.on('message', (message, rinfo) => {
|
|
280
|
+
const question = dnsQuestion(message);
|
|
281
|
+
if (!question)
|
|
282
|
+
return;
|
|
283
|
+
if (options.routesLoader().has(question.name)) {
|
|
284
|
+
const reply = question.qtype === 1
|
|
285
|
+
? dnsAnswerA(message, question.questionEnd, options.targetIp)
|
|
286
|
+
: dnsAnswerEmpty(message, question.questionEnd); // AAAA etc.: empty, so the reflected A wins
|
|
287
|
+
server.send(reply, rinfo.port, rinfo.address);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// passthrough: relay to the real upstream untouched
|
|
291
|
+
const forwarder = dgram.createSocket('udp4');
|
|
292
|
+
const timer = setTimeout(() => forwarder.close(), 3_000);
|
|
293
|
+
forwarder.on('message', (response) => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
server.send(response, rinfo.port, rinfo.address);
|
|
296
|
+
forwarder.close();
|
|
297
|
+
});
|
|
298
|
+
forwarder.send(message, options.upstreamPort ?? 53, upstream);
|
|
299
|
+
});
|
|
300
|
+
await new Promise((resolveListen, reject) => {
|
|
301
|
+
server.once('error', reject);
|
|
302
|
+
server.bind(options.port ?? 0, options.host ?? '0.0.0.0', () => resolveListen());
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
port: server.address().port,
|
|
306
|
+
async close() {
|
|
307
|
+
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export function reflectManifestPath(root, name) {
|
|
312
|
+
return join(instanceDir(root, name), 'reflect.json');
|
|
313
|
+
}
|
|
314
|
+
export function writeReflectManifest(root, name, manifest) {
|
|
315
|
+
writeFileSync(reflectManifestPath(root, name), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
316
|
+
}
|
|
317
|
+
export function readReflectManifest(root, name) {
|
|
318
|
+
const path = reflectManifestPath(root, name);
|
|
319
|
+
if (!existsSync(path))
|
|
320
|
+
return undefined;
|
|
321
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
322
|
+
if (typeof parsed.targetIp !== 'string' || typeof parsed.frontPort !== 'number' || typeof parsed.resolverPort !== 'number' || typeof parsed.caCertPath !== 'string')
|
|
323
|
+
return undefined;
|
|
324
|
+
return { ...parsed, resolverIp: typeof parsed.resolverIp === 'string' ? parsed.resolverIp : parsed.targetIp };
|
|
325
|
+
}
|
|
326
|
+
export function clearReflectManifest(root, name) {
|
|
327
|
+
const path = reflectManifestPath(root, name);
|
|
328
|
+
if (existsSync(path))
|
|
329
|
+
unlinkSync(path);
|
|
330
|
+
}
|
|
331
|
+
/** Where the CA is mounted inside an attached container, and the env that makes its clients trust it. */
|
|
332
|
+
export const ATTACHED_CA_PATH = '/etc/volter-world/ca.pem';
|
|
333
|
+
export const CA_TRUST_ENV = ['SSL_CERT_FILE', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', 'NODE_EXTRA_CA_CERTS', 'GIT_SSL_CAINFO'];
|
|
334
|
+
/**
|
|
335
|
+
* A docker compose override that attaches every named service through reflect: its DNS is the resolver
|
|
336
|
+
* (so routed vendor hosts resolve to the front), `host.docker.internal` stays resolvable (a service-level
|
|
337
|
+
* `dns` bypasses Docker's own resolver, which is what served that name), the session CA is mounted and
|
|
338
|
+
* named by the trust variables the common clients read. Requires the resolver on port 53: Docker takes a
|
|
339
|
+
* DNS address, never a port. Compose merges this after the consumer's own files, touching nothing else.
|
|
340
|
+
*/
|
|
341
|
+
export function composeOverrideForReflect(manifest, services, worldName) {
|
|
342
|
+
if (manifest.resolverPort !== 53)
|
|
343
|
+
throw new Error(`reflect attach: containers take a DNS address, not a port — run the resolver on :53 (it is on :${manifest.resolverPort})`);
|
|
344
|
+
if (manifest.frontPort !== 443)
|
|
345
|
+
throw new Error(`reflect attach: a consumer reaches https on :443 — run the front on :443 (it is on :${manifest.frontPort})`);
|
|
346
|
+
const trust = sessionTrustEnv(manifest.caCertPath);
|
|
347
|
+
const lines = ['# generated by `volter-world attach --via reflect` — never edit; merged after the consumer\'s own files', 'services:'];
|
|
348
|
+
for (const service of services) {
|
|
349
|
+
lines.push(` ${service}:`);
|
|
350
|
+
lines.push(' dns:', ` - ${manifest.resolverIp}`);
|
|
351
|
+
lines.push(' extra_hosts:', ' - host.docker.internal:host-gateway');
|
|
352
|
+
lines.push(' environment:', ` - VOLTER_WORLD=${worldName}`);
|
|
353
|
+
for (const key of CA_TRUST_ENV)
|
|
354
|
+
lines.push(` - ${key}=${ATTACHED_CA_PATH}.${key}`);
|
|
355
|
+
lines.push(' volumes:', ...CA_TRUST_ENV.map((key) => ` - ${trust[key]}:${ATTACHED_CA_PATH}.${key}:ro`));
|
|
356
|
+
}
|
|
357
|
+
return `${lines.join('\n')}\n`;
|
|
358
|
+
}
|
|
359
|
+
// docker compose's global options that take a value — the override's -f must land after the consumer's own
|
|
360
|
+
// files and before the subcommand, so these are stepped over to find it.
|
|
361
|
+
const COMPOSE_VALUED_OPTIONS = new Set(['-f', '--file', '-p', '--project-name', '--profile', '--env-file', '--project-directory', '--progress', '--parallel', '--ansi']);
|
|
362
|
+
/** Split `docker [docker flags] compose [compose flags] <subcommand…>`; undefined when it is not a compose command. */
|
|
363
|
+
export function splitDockerComposeArgs(command) {
|
|
364
|
+
if (command[0] !== 'docker')
|
|
365
|
+
return undefined;
|
|
366
|
+
const composeAt = command.indexOf('compose');
|
|
367
|
+
if (composeAt < 0)
|
|
368
|
+
return undefined;
|
|
369
|
+
const head = command.slice(0, composeAt + 1);
|
|
370
|
+
const composeFlags = [];
|
|
371
|
+
let i = composeAt + 1;
|
|
372
|
+
while (i < command.length) {
|
|
373
|
+
const arg = command[i];
|
|
374
|
+
if (!arg.startsWith('-'))
|
|
375
|
+
break;
|
|
376
|
+
if (COMPOSE_VALUED_OPTIONS.has(arg)) {
|
|
377
|
+
composeFlags.push(arg, command[i + 1] ?? '');
|
|
378
|
+
i += 2;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
composeFlags.push(arg);
|
|
382
|
+
i += 1; // --flag=value or a boolean flag
|
|
383
|
+
}
|
|
384
|
+
return { head, composeFlags, tail: command.slice(i) };
|
|
385
|
+
}
|
|
386
|
+
/** The same compose command with the override merged last. */
|
|
387
|
+
export function dockerComposeWithOverride(command, overridePath) {
|
|
388
|
+
const parts = splitDockerComposeArgs(command);
|
|
389
|
+
if (!parts)
|
|
390
|
+
return command;
|
|
391
|
+
return [...parts.head, ...parts.composeFlags, '-f', overridePath, ...parts.tail];
|
|
392
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare function worldResourceReport(root?: string): {
|
|
2
|
+
worlds: import("./lifecycle-record.js").WorldLifecycleRecord[];
|
|
3
|
+
legacyRecords: {
|
|
4
|
+
path: string;
|
|
5
|
+
world?: string;
|
|
6
|
+
root?: string;
|
|
7
|
+
cleanupPending?: boolean;
|
|
8
|
+
readable: boolean;
|
|
9
|
+
}[];
|
|
10
|
+
warnings: string[];
|
|
11
|
+
schemaVersion: number;
|
|
12
|
+
accounting: "observed";
|
|
13
|
+
measuredAt: string;
|
|
14
|
+
host: {
|
|
15
|
+
physicalMemoryBytes: number;
|
|
16
|
+
measuredMemoryUsage: boolean;
|
|
17
|
+
};
|
|
18
|
+
filesystem: {
|
|
19
|
+
path: string;
|
|
20
|
+
availableBytes: number | null;
|
|
21
|
+
totalBytes: number | null;
|
|
22
|
+
pool: string | null;
|
|
23
|
+
source: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Actual host capacity and lifecycle inventory. Declarations never reduce reported free space.
|
|
2
|
+
import { statfsSync } from 'node:fs';
|
|
3
|
+
import { totalmem } from 'node:os';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { inspectWorldLifecycles } from "./lifecycle-record.js";
|
|
6
|
+
import { storagePoolResolver } from "./storage-capacity.js";
|
|
7
|
+
export function worldResourceReport(root = process.cwd()) {
|
|
8
|
+
const inventory = inspectWorldLifecycles();
|
|
9
|
+
const measuredAt = new Date().toISOString();
|
|
10
|
+
const path = resolve(root);
|
|
11
|
+
let filesystem;
|
|
12
|
+
try {
|
|
13
|
+
const fs = statfsSync(path);
|
|
14
|
+
filesystem = { path, availableBytes: Number(fs.bavail) * Number(fs.bsize), totalBytes: Number(fs.blocks) * Number(fs.bsize),
|
|
15
|
+
pool: storagePoolResolver()(path) ?? null, source: 'statfs' };
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
filesystem = { path, availableBytes: null, totalBytes: null, pool: null, source: 'statfs', error: error.code ?? 'unavailable' };
|
|
19
|
+
}
|
|
20
|
+
return { schemaVersion: 2, accounting: 'observed', measuredAt,
|
|
21
|
+
host: { physicalMemoryBytes: totalmem(), measuredMemoryUsage: false }, filesystem, ...inventory };
|
|
22
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type Check, type CredentialPayload, type PerformAction, type RemoteExecute, type RootConfig } from '@volter/world-core';
|
|
2
|
+
import { loadChecks, type DeployReport, type ObserveReport } from '@volter/world-core';
|
|
3
|
+
import type { WorldConfig } from './schema.js';
|
|
4
|
+
/** `$XDG_CONFIG_HOME/volter/kek`, else `~/.config/volter/kek`: the key every credential this user
|
|
5
|
+
* seals is wrapped under. Made on first use, 0600, never printed. */
|
|
6
|
+
export { userKek, userKekPath } from '@volter/world-core';
|
|
7
|
+
export declare function credentialPath(worldRoot: string, vendor: string): string;
|
|
8
|
+
export declare function sealTwinCredential(worldRoot: string, vendor: string, payload: CredentialPayload, at?: string): Promise<{
|
|
9
|
+
placedAt: string;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function sealedCredentialInfo(worldRoot: string, vendor: string): {
|
|
13
|
+
placedAt: string;
|
|
14
|
+
fingerprint: string;
|
|
15
|
+
} | null;
|
|
16
|
+
/** The signing secret sealed beside the credential (`{ headers, signingSecret }`), for ingest; null when none. */
|
|
17
|
+
export declare function twinSigningSecret(worldRoot: string, vendor: string): Promise<string | null>;
|
|
18
|
+
/** A bare token becomes the vendor's own header. A JSON object is taken as the payload. */
|
|
19
|
+
export declare function credentialPayloadFrom(input: string, vendor: string): CredentialPayload;
|
|
20
|
+
/** The root config carried in the state dir also names where the credential is sealed, so the
|
|
21
|
+
* write path's auto-deploy hook can open it without knowing the world's layout. */
|
|
22
|
+
export type MaterializedRoot = RootConfig & {
|
|
23
|
+
credential: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function setTwinRoot(worldRoot: string, vendor: string, config: RootConfig | null, opts?: {
|
|
26
|
+
config?: string;
|
|
27
|
+
}): {
|
|
28
|
+
path: string;
|
|
29
|
+
};
|
|
30
|
+
/** At boot: every twin with a root gets root.json in its state dir(s), so core's write path and the
|
|
31
|
+
* deploy read it there; a twin whose root was cleared loses it. */
|
|
32
|
+
export declare function materializeRoots(config: WorldConfig, dataDir: string, worldRoot: string): void;
|
|
33
|
+
/** The root a state service under `controlRoot` is bound to — its own root.json, else a sibling
|
|
34
|
+
* state's (slack records under `chat`; the root was written under `slack`). */
|
|
35
|
+
export declare function rootForControlRoot(controlRoot: string, service: string): MaterializedRoot | null;
|
|
36
|
+
/** The pack's mirror module (`src/<vendor>-mirror-ui.ts`: its shell, client and stylesheet), read as the
|
|
37
|
+
* hosted mirror build reads it (apps/cloud/scripts/mirrors.ts); a pack's index need not re-export them. */
|
|
38
|
+
export declare function packMirrorExports(vendor: string, from: string): Promise<Record<string, unknown> | null>;
|
|
39
|
+
/** The real state system's adapters for a vendor: registered with the pack (protocol 2), else the
|
|
40
|
+
* pack's anchored exports (protocol 1: `perform<Name>Action`, `sync<Name>FromRemote`). */
|
|
41
|
+
export type IngestAdapter = (request: Request, ctx: {
|
|
42
|
+
root?: string;
|
|
43
|
+
secret?: string;
|
|
44
|
+
}) => Response | Promise<Response>;
|
|
45
|
+
export declare function adaptersFor(vendor: string, from: string): Promise<{
|
|
46
|
+
perform?: PerformAction;
|
|
47
|
+
refresh?: (execute: RemoteExecute, opts: {
|
|
48
|
+
root?: string;
|
|
49
|
+
origin?: string;
|
|
50
|
+
credential?: string;
|
|
51
|
+
}) => Promise<unknown>;
|
|
52
|
+
ingest?: IngestAdapter;
|
|
53
|
+
}>;
|
|
54
|
+
/** `.volter/checks/*.ts|js`: each exports `check` (or default) as `{ name, run }`. The shipped
|
|
55
|
+
* no-secrets check runs first, always. */
|
|
56
|
+
export declare const loadWorldChecks: typeof loadChecks;
|
|
57
|
+
/** The pack's declared refresh posture, read from its descriptor. */
|
|
58
|
+
export declare function refreshPosture(vendor: string, from: string): Promise<{
|
|
59
|
+
every?: string;
|
|
60
|
+
webhook?: boolean;
|
|
61
|
+
onDemand?: {
|
|
62
|
+
atMost: string;
|
|
63
|
+
};
|
|
64
|
+
} | undefined>;
|
|
65
|
+
/** `<N>(s|m|h|d)` as milliseconds, or null when malformed. */
|
|
66
|
+
export declare function spanMs(text: string): number | null;
|
|
67
|
+
export type DeployTwinOutcome = {
|
|
68
|
+
service: string;
|
|
69
|
+
state: string;
|
|
70
|
+
refreshed: boolean;
|
|
71
|
+
report: DeployReport; /** the changeset this deploy was for, when one was named */
|
|
72
|
+
changeset?: string;
|
|
73
|
+
};
|
|
74
|
+
/** PERFORM a twin's waiting entries against its root: the kernel's head (`performEntries`) with the
|
|
75
|
+
* pack's adapters and the world's checks. A refresh first, when the pack has one, so a deploy sees
|
|
76
|
+
* the vendor as it stands. */
|
|
77
|
+
export declare function deployTwin(opts: {
|
|
78
|
+
worldRoot: string;
|
|
79
|
+
vendor: string;
|
|
80
|
+
controlRoot: string;
|
|
81
|
+
state: string;
|
|
82
|
+
root: MaterializedRoot;
|
|
83
|
+
actionIds?: string[];
|
|
84
|
+
checks?: Check[];
|
|
85
|
+
at?: string;
|
|
86
|
+
}): Promise<DeployTwinOutcome>;
|
|
87
|
+
/** REFRESH one twin from its root: the pack's refresh adapter over the executor observes resources;
|
|
88
|
+
* the kernel folds them onto the root's log (company contract "Refresh is the kernel's fold"). On
|
|
89
|
+
* demand it is throttled by the root's `refresh.atMost`, else the pack's `onDemand.atMost`. */
|
|
90
|
+
export declare function refreshTwin(opts: {
|
|
91
|
+
worldRoot: string;
|
|
92
|
+
vendor: string;
|
|
93
|
+
controlRoot: string;
|
|
94
|
+
root: MaterializedRoot;
|
|
95
|
+
force?: boolean;
|
|
96
|
+
}): Promise<{
|
|
97
|
+
refreshed: boolean;
|
|
98
|
+
reason?: string;
|
|
99
|
+
report?: ObserveReport;
|
|
100
|
+
}>;
|
|
101
|
+
/** DEPLOY a world: perform landed entries against each root twin's vendor, by policy. A named
|
|
102
|
+
* changeset deploys its entries (a gated root needs it verified and approved); no name deploys
|
|
103
|
+
* everything deployable — under `gated`, the entries of every ready changeset. */
|
|
104
|
+
export declare function deployWorld(name: string, opts?: {
|
|
105
|
+
root?: string;
|
|
106
|
+
changeset?: string;
|
|
107
|
+
checks?: Check[];
|
|
108
|
+
instance?: {
|
|
109
|
+
dirs: {
|
|
110
|
+
data: string;
|
|
111
|
+
};
|
|
112
|
+
services: Record<string, unknown>;
|
|
113
|
+
};
|
|
114
|
+
}): Promise<DeployTwinOutcome[]>;
|