@volter/twin-world 0.1.0 → 0.1.2
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/known-external-services.json +1192 -0
- package/package.json +12 -5
- package/src/app-url.ts +175 -0
- package/src/attach.ts +113 -0
- package/src/browser-proxy-cli.ts +2 -1
- package/src/changeset.ts +416 -0
- package/src/cli.ts +589 -10
- package/src/covers.ts +724 -0
- package/src/fixture-env.ts +207 -0
- package/src/host-cli.ts +2 -1
- package/src/host-worker.ts +4 -3
- package/src/host.ts +26 -4
- package/src/index.ts +97 -1
- package/src/init.ts +1142 -0
- package/src/inject-map.ts +70 -0
- package/src/managed-infra-cli.ts +208 -0
- package/src/pack-facts.ts +136 -0
- package/src/pglite-backing.ts +125 -0
- package/src/pglite-host.mjs +147 -0
- package/src/prerequisites.ts +15 -58
- package/src/project-inspect.ts +683 -0
- package/src/redirect-proxy.ts +109 -85
- package/src/reflect.ts +443 -0
- package/src/resource-holder.ts +11 -0
- package/src/resources.ts +160 -0
- package/src/runtime-test-support.ts +208 -0
- package/src/runtime.ts +667 -106
- package/src/schema.ts +67 -0
- package/src/serve.ts +102 -0
- package/src/tail.ts +203 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// World fixture env — STRUCTURALLY VALID fake credentials for the env a world hands the app
|
|
2
|
+
// under test.
|
|
3
|
+
//
|
|
4
|
+
// The failure this fixes (feature-sweep): fake env values in a world config are hand-authored
|
|
5
|
+
// scalars (`sk_test_fake_…`), which is exactly right for credentials an SDK treats as an opaque
|
|
6
|
+
// bearer token — the twin accepts anything, the value's SHAPE never matters. But some
|
|
7
|
+
// credentials are PARSED AND USED CLIENT-SIDE before any request leaves the process: a Google
|
|
8
|
+
// service-account JSON is loaded by google-auth-library, which signs a JWT assertion with its
|
|
9
|
+
// `private_key` locally and only then calls the (twin-served) token endpoint. A faked
|
|
10
|
+
// `GOOGLE_VERTEX_JSON` lacking `private_key` therefore throws inside the app and blocks every
|
|
11
|
+
// Google-auth path — no twin ever sees a request. The fix is not a smarter twin; it is a
|
|
12
|
+
// STRUCTURALLY VALID fake: all required fields, and a REAL throwaway RSA key the client library
|
|
13
|
+
// can genuinely sign with. The signature is verified by nothing (the twin's token endpoint
|
|
14
|
+
// accepts any well-formed assertion), so a throwaway key is safe by construction — it
|
|
15
|
+
// corresponds to no real account anywhere.
|
|
16
|
+
//
|
|
17
|
+
// THE PATTERN (document + reuse for any future structurally-parsed credential):
|
|
18
|
+
// 1. find what the CLIENT SDK does with the value before the first network call;
|
|
19
|
+
// 2. fake every field it reads, with material that genuinely works (a real keypair, a
|
|
20
|
+
// well-formed JWT, …) — never placeholder strings in cryptographic positions;
|
|
21
|
+
// 3. keep the fake obviously fake in the IDENTIFYING fields (project id, email) so it can
|
|
22
|
+
// never be mistaken for a real credential;
|
|
23
|
+
// 4. mint keys per process (throwaway), never commit them — a committed key looks real to
|
|
24
|
+
// scanners and to attackers alike.
|
|
25
|
+
// Precedents in-repo: gcs-sdk.integration.test.ts (throwaway RSA key lets the unmodified
|
|
26
|
+
// @google-cloud/storage SDK sign V4 URLs offline), clerk-jwt.ts / fal-webhooks.ts (generated
|
|
27
|
+
// signing keypairs).
|
|
28
|
+
import { generateKeyPairSync } from 'node:crypto';
|
|
29
|
+
|
|
30
|
+
export type FakeServiceAccountOptions = {
|
|
31
|
+
/** identifying project id — defaults to an unmistakably fake twin project. */
|
|
32
|
+
projectId?: string;
|
|
33
|
+
/** client_email — defaults to a twin address under the fake project. */
|
|
34
|
+
clientEmail?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// RSA-2048 generation costs ~100ms — mint ONCE per process and reuse (the clerk-worker
|
|
38
|
+
// baked-key history is the cautionary precedent for per-call generation cost). Throwaway by
|
|
39
|
+
// design: never persisted, never committed, corresponds to no real account.
|
|
40
|
+
let cachedPrivateKeyPem: string | undefined;
|
|
41
|
+
function throwawayPrivateKeyPem(): string {
|
|
42
|
+
if (!cachedPrivateKeyPem) {
|
|
43
|
+
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
44
|
+
cachedPrivateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
45
|
+
}
|
|
46
|
+
return cachedPrivateKeyPem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A STRUCTURALLY VALID fake Google service-account JSON: every field a real
|
|
51
|
+
* `service_account` key file carries, with a REAL throwaway RSA private key — so
|
|
52
|
+
* google-auth-library (and every SDK built on it: Vertex, GCS, @ai-sdk/google-vertex, …)
|
|
53
|
+
* loads it, signs its JWT assertion locally, and proceeds to the twin-served token
|
|
54
|
+
* endpoint instead of throwing before the first request.
|
|
55
|
+
*/
|
|
56
|
+
export function fakeGoogleServiceAccountJson(opts: FakeServiceAccountOptions = {}): string {
|
|
57
|
+
const projectId = opts.projectId ?? 'twin-project';
|
|
58
|
+
const clientEmail = opts.clientEmail ?? `twin-fake@${projectId}.iam.gserviceaccount.com`;
|
|
59
|
+
return JSON.stringify(
|
|
60
|
+
{
|
|
61
|
+
type: 'service_account',
|
|
62
|
+
project_id: projectId,
|
|
63
|
+
private_key_id: 'twinfake0000000000000000000000000000dead',
|
|
64
|
+
private_key: throwawayPrivateKeyPem(),
|
|
65
|
+
client_email: clientEmail,
|
|
66
|
+
client_id: '000000000000000000000',
|
|
67
|
+
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
|
68
|
+
token_uri: 'https://oauth2.googleapis.com/token',
|
|
69
|
+
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
|
70
|
+
client_x509_cert_url: `https://www.googleapis.com/robot/v1/metadata/x509/${encodeURIComponent(clientEmail)}`,
|
|
71
|
+
universe_domain: 'googleapis.com',
|
|
72
|
+
},
|
|
73
|
+
null,
|
|
74
|
+
2,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A STRUCTURALLY VALID fake Google OAUTH WEB-CLIENT JSON — the `{"web":{...}}` shape a
|
|
80
|
+
* Google-Cloud-console "OAuth client" download has, which is what OAuth-APP consumers parse
|
|
81
|
+
* (cal.com: `JSON.parse(GOOGLE_API_CREDENTIALS)?.web`, then a zod schema over
|
|
82
|
+
* client_id/client_secret/redirect_uris). This is a DIFFERENT credential species from the
|
|
83
|
+
* service-account JSON above: faking the SA shape for these names passes the consumer's
|
|
84
|
+
* "valid JSON" check and then yields `?.web === undefined`, so the integration silently
|
|
85
|
+
* degrades instead of erroring — the worst failure mode for a fake credential (Cal.com blind
|
|
86
|
+
* run, friction #4). No cryptographic material is needed: the client secret is an opaque
|
|
87
|
+
* bearer the twin accepts; identifying fields are unmistakably fake.
|
|
88
|
+
*/
|
|
89
|
+
export function fakeGoogleOAuthClientJson(): string {
|
|
90
|
+
return JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
web: {
|
|
93
|
+
client_id: '000000000000-twinfake.apps.googleusercontent.com',
|
|
94
|
+
project_id: 'twin-project',
|
|
95
|
+
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
|
96
|
+
token_uri: 'https://oauth2.googleapis.com/token',
|
|
97
|
+
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
|
98
|
+
client_secret: 'twin-fake-google-oauth-client-secret',
|
|
99
|
+
redirect_uris: ['http://localhost:3000/api/auth/callback/google'],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
null,
|
|
103
|
+
2,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Does this env NAME carry an INLINE Google OAUTH-CLIENT JSON (the `{"web":{...}}` shape)?
|
|
108
|
+
* Matches a GOOGLE stem combined with an OAuth-app marker: `API_CREDENTIALS` (cal.com's
|
|
109
|
+
* GOOGLE_API_CREDENTIALS — the name that motivated this), `OAUTH_*`, or `*_CLIENT_JSON` /
|
|
110
|
+
* `CLIENT_CREDENTIALS` forms. Deliberately NOT the SERVICE_ACCOUNT-marked names, NOT the
|
|
111
|
+
* GCP/GCLOUD/VERTEX stems (cloud-platform credentials are service accounts), and NOT bare
|
|
112
|
+
* GOOGLE_CREDENTIALS (ambiguous; historically ADC, i.e. SA-shaped — kept on the SA fake). */
|
|
113
|
+
export function isGoogleOAuthClientEnvName(name: string): boolean {
|
|
114
|
+
const n = name.toUpperCase();
|
|
115
|
+
if (!/(^|_)GOOGLE(_|$)/.test(n)) return false;
|
|
116
|
+
if (n === 'GOOGLE_APPLICATION_CREDENTIALS') return false; // a path, not inline JSON
|
|
117
|
+
if (/SERVICE_ACCOUNT/.test(n)) return false; // explicitly SA-shaped
|
|
118
|
+
return /(API_CREDENTIALS|OAUTH_(CLIENT|CREDENTIALS|JSON)|CLIENT_(JSON|CREDENTIALS))/.test(n);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Does this env NAME carry an INLINE Google service-account JSON (the structurally-parsed
|
|
122
|
+
* kind)? Matches a Google-ish stem (GOOGLE/GCP/GCLOUD/VERTEX) combined with a JSON/
|
|
123
|
+
* credentials/service-account marker — e.g. GOOGLE_VERTEX_JSON, GCP_SERVICE_ACCOUNT_JSON,
|
|
124
|
+
* GOOGLE_APPLICATION_CREDENTIALS_JSON. Deliberately NOT GOOGLE_MAPS_API_KEY (opaque scalar),
|
|
125
|
+
* NOT GOOGLE_APPLICATION_CREDENTIALS (a FILE PATH — point it at a file whose contents
|
|
126
|
+
* come from fakeGoogleServiceAccountJson instead), and NOT the OAuth-client names above —
|
|
127
|
+
* those need the `{"web":{...}}` shape, not SA JSON. */
|
|
128
|
+
export function isGoogleServiceAccountEnvName(name: string): boolean {
|
|
129
|
+
const n = name.toUpperCase();
|
|
130
|
+
if (!/(^|_)(GOOGLE|GCP|GCLOUD|VERTEX)(_|$)/.test(n)) return false;
|
|
131
|
+
if (n === 'GOOGLE_APPLICATION_CREDENTIALS') return false; // a path, not inline JSON
|
|
132
|
+
if (isGoogleOAuthClientEnvName(name)) return false; // OAuth-client shape, handled separately
|
|
133
|
+
return /(_JSON$|_JSON_|CREDENTIALS|SERVICE_ACCOUNT)/.test(n);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A structurally valid fake value for one env NAME. Google OAuth-client-shaped names get the
|
|
138
|
+
* `{"web":{...}}` client JSON; Google service-account-shaped names get the full SA JSON (real
|
|
139
|
+
* throwaway key); endpoint-shaped names get a parseable vendor-host URL that the injector can
|
|
140
|
+
* claim; everything else gets an unmistakably fake opaque scalar (`twin-fake-<name>`), which is
|
|
141
|
+
* sufficient for bearer-token credentials because the twin accepts any value — shape only
|
|
142
|
+
* matters when the CLIENT parses it (rule of thumb: if a blind adoption run shows an SDK throwing
|
|
143
|
+
* on a fake before any request is made, that env var needs a structural fake; add its rule here).
|
|
144
|
+
*/
|
|
145
|
+
/** Endpoint values whose SHAPE is consumed before the first network call.
|
|
146
|
+
*
|
|
147
|
+
* `new URL(value)` runs inside each client SDK before the injector can see a request, so an opaque
|
|
148
|
+
* `twin-fake-*` scalar fails even though the vendor is otherwise perfectly intercepted. These
|
|
149
|
+
* values are deliberately real-vendor-HOST-SHAPED but credential-free: the injector claims the
|
|
150
|
+
* hostname and sends the request to the local twin, while the slug remains unmistakably fake.
|
|
151
|
+
*
|
|
152
|
+
* Like CREDENTIAL_SHAPES below, this is duplicated kernel data with an explicit canonical source:
|
|
153
|
+
* world-runtime must not import a pack, and the tests pin every value against the injector's actual
|
|
154
|
+
* VENDOR_HOSTS predicate so either side changing makes the contract fail loudly. */
|
|
155
|
+
const ENDPOINT_SHAPES: Array<{ match: RegExp; value: string; source: string }> = [
|
|
156
|
+
{
|
|
157
|
+
match: /(^|_)UPSTASH_REDIS_REST_URL$/,
|
|
158
|
+
value: 'https://twin-fake.upstash.io',
|
|
159
|
+
source: 'packages/twin/control-plane/inject.cjs VENDOR_HOSTS.upstashredis',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
match: /(^|_)UPSTASH_VECTOR_REST_URL$/,
|
|
163
|
+
value: 'https://twin-fake-us1-vector.upstash.io',
|
|
164
|
+
source: 'packages/twin/control-plane/inject.cjs VENDOR_HOSTS.upstashvector',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
match: /(^|_)TINYBIRD_API_URL$/,
|
|
168
|
+
value: 'https://api.tinybird.co',
|
|
169
|
+
source: 'packages/twin/control-plane/inject.cjs VENDOR_HOSTS.tinybird',
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/** Credentials whose SHAPE the receiving twin actually checks.
|
|
174
|
+
*
|
|
175
|
+
* The generic `twin-fake-<name>` value is inert everywhere it lands — except at a twin that
|
|
176
|
+
* validates its own vendor's key format. Then the world is fully covered and the app still gets a
|
|
177
|
+
* 401, which reads as "the twin doesn't work" and is the most confusing failure this system can
|
|
178
|
+
* produce (measured: it cost two of four subjects a clean boot in PROOF-2026-08-20.md, finding F3).
|
|
179
|
+
* A minted fake must satisfy the twin that will receive it — the minter and the twin are two halves
|
|
180
|
+
* of one contract.
|
|
181
|
+
*
|
|
182
|
+
* These literals are DUPLICATED from the packs on purpose: the kernel must not import a pack
|
|
183
|
+
* (ARCHITECTURE.md). Each entry names its source so the pair can be re-checked by hand. */
|
|
184
|
+
const CREDENTIAL_SHAPES: Array<{ match: RegExp; value: (name: string) => string; source: string }> = [
|
|
185
|
+
// A Discord bot token is three dot-separated base64 segments; a library parses the first for the
|
|
186
|
+
// application id, so a structurally wrong fake breaks before any call reaches the twin.
|
|
187
|
+
{ match: /(^|_)DISCORD_[A-Z0-9_]*(TOKEN|SECRET)$/, value: () => `${btoa('1'.repeat(18)).replace(/=+$/, '')}.Gtwinf.${'t'.repeat(38)}`, source: 'a Discord bot token: three dot-separated base64 segments, the first the application id' },
|
|
188
|
+
// resend-twin.ts enforces bearer auth of the form `re_<...>` whenever a key is presented.
|
|
189
|
+
{ match: /(^|_)RESEND_[A-Z0-9_]*(KEY|TOKEN|SECRET)$/, value: (name) => `re_twinfake_${name.toLowerCase().replace(/_/g, '')}`, source: 'packages/twin/resend/src/resend-twin.ts' },
|
|
190
|
+
// tinybird-twin.ts accepts exactly one admin token, TINYBIRD_ADMIN_TOKEN = 'p.twin_admin_token'.
|
|
191
|
+
{ match: /(^|_)TINYBIRD_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'p.twin_admin_token', source: 'packages/twin/tinybird/src/tinybird-twin.ts' },
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
export function fakeEnvValue(name: string): string {
|
|
195
|
+
if (isGoogleOAuthClientEnvName(name)) return fakeGoogleOAuthClientJson();
|
|
196
|
+
if (isGoogleServiceAccountEnvName(name)) return fakeGoogleServiceAccountJson();
|
|
197
|
+
for (const shape of ENDPOINT_SHAPES) if (shape.match.test(name)) return shape.value;
|
|
198
|
+
for (const shape of CREDENTIAL_SHAPES) if (shape.match.test(name)) return shape.value(name);
|
|
199
|
+
const slug = name.toLowerCase().replace(/_/g, '-');
|
|
200
|
+
// A URL-shaped name must be faked as a URL (F4). An SDK handed `twin-fake-x` where it expects an
|
|
201
|
+
// endpoint throws while CONSTRUCTING its client — before it ever issues the request the world
|
|
202
|
+
// could have intercepted, so the failure looks like a twin bug and no ledger entry exists to
|
|
203
|
+
// contradict that. `.invalid` is reserved (RFC 2606) and never resolves, so an untwinned vendor
|
|
204
|
+
// still fails — but as an unreachable HOST, which the diagnostics can name.
|
|
205
|
+
if (/(^|_)(URL|URI|ENDPOINT|HOST)$/.test(name)) return `https://${slug}.invalid`;
|
|
206
|
+
return `twin-fake-${slug}`;
|
|
207
|
+
}
|
package/src/host-cli.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { keepProcessAlive } from '@volter/twin/lifecycle';
|
|
2
3
|
// volter-world-host: run a set of twins co-located in this single process. The world
|
|
3
4
|
// runtime spawns ONE of these (instead of one bin per twin) when a world selects
|
|
4
5
|
// 'colocated'/'worker' isolation. Each twin is named by a module specifier + export so
|
|
@@ -83,4 +84,4 @@ process.stdout.write(`host ready (${specs.length} twins, isolation=${isolation})
|
|
|
83
84
|
const shutdown = async () => { await host.stop(); process.exit(0); };
|
|
84
85
|
process.on('SIGTERM', shutdown);
|
|
85
86
|
process.on('SIGINT', shutdown);
|
|
86
|
-
await
|
|
87
|
+
await keepProcessAlive();
|
package/src/host-worker.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type { ColocatedTwinSpec } from './host.ts';
|
|
|
7
7
|
|
|
8
8
|
const spec = workerData as ColocatedTwinSpec;
|
|
9
9
|
|
|
10
|
-
type TwinServer = { port: number; stop: () => void };
|
|
11
|
-
type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean }) => TwinServer
|
|
10
|
+
type TwinServer = { port: number; stop: () => void | Promise<void> };
|
|
11
|
+
type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean }) => TwinServer | Promise<TwinServer>;
|
|
12
12
|
|
|
13
13
|
const mod = (await import(spec.module)) as Record<string, unknown>;
|
|
14
14
|
const factory = mod[spec.export] as TwinServerFactory | undefined;
|
|
@@ -16,7 +16,8 @@ if (typeof factory !== 'function') {
|
|
|
16
16
|
throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Awaited: an async factory (fly) must post `ready` only once its listener is bound.
|
|
20
|
+
const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly });
|
|
20
21
|
parentPort?.postMessage({ type: 'ready', port: server.port });
|
|
21
22
|
|
|
22
23
|
// The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
|
package/src/host.ts
CHANGED
|
@@ -42,8 +42,14 @@ export type ColocatedHost = {
|
|
|
42
42
|
stop: () => Promise<void>;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// `stop` returns void OR a promise (76 of 78 packs return Bun's async server.stop) and the
|
|
46
|
+
// factory itself may be async (fly's createFlyTwinServer) — both shapes are awaited, so
|
|
47
|
+
// "resolves once every twin is listening" is true for async factories and shutdown actually
|
|
48
|
+
// WAITS instead of fire-and-forgetting (§9 skeptic finding M1, 2026-08-31: an async factory
|
|
49
|
+
// made host.stop() throw `s.stop is not a function`, turning every SIGTERM into downWorld's
|
|
50
|
+
// SIGKILL escalation for the whole world).
|
|
51
|
+
type TwinServer = { port: number; stop: () => void | Promise<void> };
|
|
52
|
+
type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean }) => TwinServer | Promise<TwinServer>;
|
|
47
53
|
|
|
48
54
|
async function loadFactory(spec: ColocatedTwinSpec): Promise<TwinServerFactory> {
|
|
49
55
|
const mod = (await import(spec.module)) as Record<string, unknown>;
|
|
@@ -56,6 +62,11 @@ async function loadFactory(spec: ColocatedTwinSpec): Promise<TwinServerFactory>
|
|
|
56
62
|
|
|
57
63
|
const workerEntry = new URL('./host-worker.ts', import.meta.url);
|
|
58
64
|
|
|
65
|
+
/** Per-twin stop budget inside host.stop(). Must stay comfortably under downWorld's 5s
|
|
66
|
+
* SIGTERM→SIGKILL grace (DOWN_GRACE_MS_DEFAULT): the host must reach process.exit(0) even
|
|
67
|
+
* when a twin's stop hangs, or the whole co-located world dies by SIGKILL. */
|
|
68
|
+
const STOP_TIMEOUT_MS = 3000;
|
|
69
|
+
|
|
59
70
|
/**
|
|
60
71
|
* Start a co-located host. Resolves once every twin is listening. The returned `stop()`
|
|
61
72
|
* tears the whole host down (all servers / all workers).
|
|
@@ -86,7 +97,7 @@ export async function startColocatedHost(
|
|
|
86
97
|
const servers: TwinServer[] = [];
|
|
87
98
|
for (const spec of specs) {
|
|
88
99
|
const factory = await loadFactory(spec);
|
|
89
|
-
servers.push(factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly }));
|
|
100
|
+
servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly }));
|
|
90
101
|
}
|
|
91
102
|
return {
|
|
92
103
|
isolation,
|
|
@@ -94,7 +105,18 @@ export async function startColocatedHost(
|
|
|
94
105
|
stop: async () => {
|
|
95
106
|
process.off('unhandledRejection', onRejection);
|
|
96
107
|
process.off('uncaughtException', onUncaught);
|
|
97
|
-
|
|
108
|
+
// EVERY stop is invoked, IN PARALLEL, each under its own timeout, and no outcome —
|
|
109
|
+
// rejection, hang, or slowness — can prevent the others or wedge host.stop() itself.
|
|
110
|
+
// The awaited-sequential first cut re-created the SIGKILL escalation it was fixing
|
|
111
|
+
// through a new door (§9 round two H2, 2026-08-31: one pending/throwing stop — fly's
|
|
112
|
+
// cleanup throws on a failed container removal — left siblings unstopped and the host
|
|
113
|
+
// ignoring SIGTERM forever). The bound stays under downWorld's 5s SIGKILL grace.
|
|
114
|
+
await Promise.allSettled(servers.map((s) =>
|
|
115
|
+
Promise.race([
|
|
116
|
+
Promise.resolve().then(() => s.stop()),
|
|
117
|
+
new Promise((resolveTimeout) => setTimeout(resolveTimeout, STOP_TIMEOUT_MS).unref?.()),
|
|
118
|
+
]),
|
|
119
|
+
));
|
|
98
120
|
},
|
|
99
121
|
};
|
|
100
122
|
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@ export type {
|
|
|
9
9
|
WorldExternalDiscover,
|
|
10
10
|
WorldExternalDiscoverSource,
|
|
11
11
|
WorldExternalReadyWhen,
|
|
12
|
+
WorldRunOutcome,
|
|
13
|
+
WorldRunRecord,
|
|
14
|
+
WorldResourceRequirements,
|
|
12
15
|
} from './schema.ts';
|
|
13
16
|
export { loadWorldConfig, resolveConfigPath } from './configs.ts';
|
|
14
17
|
export {
|
|
@@ -16,6 +19,8 @@ export {
|
|
|
16
19
|
downWorld,
|
|
17
20
|
listWorlds,
|
|
18
21
|
statusWorld,
|
|
22
|
+
planWorldActions,
|
|
23
|
+
reviewWorldActions,
|
|
19
24
|
doctorWorld,
|
|
20
25
|
runWithWorldEnv,
|
|
21
26
|
activateScript,
|
|
@@ -28,15 +33,106 @@ export {
|
|
|
28
33
|
shareWorldServices,
|
|
29
34
|
unshareWorld,
|
|
30
35
|
urlsWorld,
|
|
36
|
+
urlWorld,
|
|
31
37
|
} from './runtime.ts';
|
|
38
|
+
// `volter-world app-url` — the recorded, queryable app endpoint of an instance (see app-url.ts).
|
|
39
|
+
export { appUrlFile, appUrlUnsetMessage, detectAppUrl, readAppUrl, setAppUrl } from './app-url.ts';
|
|
40
|
+
export type { AppUrlRecord, AppUrlSource } from './app-url.ts';
|
|
41
|
+
export { tailWorldActions } from './tail.ts';
|
|
42
|
+
export type { TailWorldOptions } from './tail.ts';
|
|
43
|
+
// the changeset primitive + the diff verb (docs/OPERATIONAL_VCS.md v0) and the operational-PR
|
|
44
|
+
// contract over it (v1: verify/approve/status). World-side glue only — markers, deltas, content
|
|
45
|
+
// hashing, replay, verifier evaluation and approval binding live in @volter/twin's control plane.
|
|
32
46
|
export {
|
|
33
|
-
|
|
47
|
+
approveWorldChangeset,
|
|
48
|
+
createWorldChangeset,
|
|
49
|
+
diffWorld,
|
|
50
|
+
EPHEMERAL_VERIFY_TARGET,
|
|
51
|
+
findWorldChangeset,
|
|
52
|
+
listWorldChangesets,
|
|
53
|
+
listWorldMarks,
|
|
54
|
+
markWorld,
|
|
55
|
+
replayWorldChangeset,
|
|
56
|
+
resolveBaseMarker,
|
|
57
|
+
statusWorldChangeset,
|
|
58
|
+
verifyWorldChangeset,
|
|
59
|
+
worldChangesetsDir,
|
|
60
|
+
worldLedgers,
|
|
61
|
+
worldMarksDir,
|
|
62
|
+
} from './changeset.ts';
|
|
63
|
+
export type { ApproveWorldOutcome, ChangesetLocation, VerifyWorldOutcome, WorldRootOptions } from './changeset.ts';
|
|
64
|
+
export {
|
|
65
|
+
checkLocalExecution,
|
|
34
66
|
checkPrerequisites,
|
|
35
67
|
formatPrerequisiteChecks,
|
|
36
68
|
} from './prerequisites.ts';
|
|
69
|
+
export { inspectProject, formatProjectInspection, projectDependencies, projectEnvNames, SDK_TWINS, workspaceDirs } from './project-inspect.ts';
|
|
70
|
+
export type { ProjectInspection } from './project-inspect.ts';
|
|
71
|
+
// Structurally valid fake credentials for world configs (fixture env) — see fixture-env.ts
|
|
72
|
+
// for the pattern (a fake a client SDK parses/signs with must be structurally real).
|
|
73
|
+
export { fakeEnvValue, fakeGoogleOAuthClientJson, fakeGoogleServiceAccountJson, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from './fixture-env.ts';
|
|
74
|
+
export type { FakeServiceAccountOptions } from './fixture-env.ts';
|
|
75
|
+
export {
|
|
76
|
+
coverWorld,
|
|
77
|
+
detectRepoVendors,
|
|
78
|
+
envNameVendor,
|
|
79
|
+
formatCoverageReport,
|
|
80
|
+
injectorVendorKeysFor,
|
|
81
|
+
isCredentialShapedEnvName,
|
|
82
|
+
worldTwinInventory,
|
|
83
|
+
} from './covers.ts';
|
|
84
|
+
export type { CoverageReport, CoverageRow, CoverageStatus, CoverageOptions, RepoVendorSignals } from './covers.ts';
|
|
85
|
+
// `volter-world init` — the deterministic front door: detect the repo's vendors (covers), emit a
|
|
86
|
+
// world config + env file OUTSIDE the repo (fixture-env), and prove the emission with `covers`.
|
|
87
|
+
export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from './init.ts';
|
|
88
|
+
export type {
|
|
89
|
+
EnvDisposition,
|
|
90
|
+
InitEnvRow,
|
|
91
|
+
InitInfraStub,
|
|
92
|
+
InitOptions,
|
|
93
|
+
InitPlan,
|
|
94
|
+
InitResult,
|
|
95
|
+
InitVendorPlan,
|
|
96
|
+
InitWiring,
|
|
97
|
+
} from './init.ts';
|
|
98
|
+
export {
|
|
99
|
+
deriveWorldManifest,
|
|
100
|
+
findWorldRefFile,
|
|
101
|
+
isRemoteWorldRef,
|
|
102
|
+
resolveWorldRef,
|
|
103
|
+
WORLD_REF_FILE,
|
|
104
|
+
worldManifest,
|
|
105
|
+
} from './attach.ts';
|
|
106
|
+
export type { ResolvedWorldRef, WorldManifest, WorldRefSource } from './attach.ts';
|
|
107
|
+
export {
|
|
108
|
+
parseSni,
|
|
109
|
+
readReflectRoutes,
|
|
110
|
+
reflectRoutesPath,
|
|
111
|
+
reflectManifestPath,
|
|
112
|
+
readReflectManifest,
|
|
113
|
+
writeReflectManifest,
|
|
114
|
+
clearReflectManifest,
|
|
115
|
+
composeOverrideForReflect,
|
|
116
|
+
splitDockerComposeArgs,
|
|
117
|
+
dockerComposeWithOverride,
|
|
118
|
+
CA_TRUST_ENV,
|
|
119
|
+
ATTACHED_CA_PATH,
|
|
120
|
+
startReflectFront,
|
|
121
|
+
startReflectResolver,
|
|
122
|
+
writeReflectRoutes,
|
|
123
|
+
} from './reflect.ts';
|
|
124
|
+
export type { ReflectFrontHandle, ReflectFrontOptions, ReflectManifest, ReflectResolverHandle, ReflectResolverOptions } from './reflect.ts';
|
|
37
125
|
export type {
|
|
38
126
|
PrerequisiteCheck,
|
|
39
127
|
PrerequisiteId,
|
|
40
128
|
} from './prerequisites.ts';
|
|
41
129
|
export { startColocatedHost } from './host.ts';
|
|
42
130
|
export type { ColocatedTwinSpec, ColocatedHost, HostIsolation } from './host.ts';
|
|
131
|
+
export {
|
|
132
|
+
advertiseWorldManifest,
|
|
133
|
+
fetchRemoteManifest,
|
|
134
|
+
MANIFEST_PATH,
|
|
135
|
+
remoteAttachEnv,
|
|
136
|
+
startManifestServer,
|
|
137
|
+
} from './serve.ts';
|
|
138
|
+
export type { ManifestServerHandle, ManifestServerOptions } from './serve.ts';
|