@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,42 @@
|
|
|
1
|
+
export type FakeServiceAccountOptions = {
|
|
2
|
+
/** identifying project id — defaults to an unmistakably fake twin project. */
|
|
3
|
+
projectId?: string;
|
|
4
|
+
/** client_email — defaults to a twin address under the fake project. */
|
|
5
|
+
clientEmail?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* A STRUCTURALLY VALID fake Google service-account JSON: every field a real
|
|
9
|
+
* `service_account` key file carries, with a REAL throwaway RSA private key — so
|
|
10
|
+
* google-auth-library (and every SDK built on it: Vertex, GCS, @ai-sdk/google-vertex, …)
|
|
11
|
+
* loads it, signs its JWT assertion locally, and proceeds to the twin-served token
|
|
12
|
+
* endpoint instead of throwing before the first request.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fakeGoogleServiceAccountJson(opts?: FakeServiceAccountOptions): string;
|
|
15
|
+
/**
|
|
16
|
+
* A STRUCTURALLY VALID fake Google OAUTH WEB-CLIENT JSON — the `{"web":{...}}` shape a
|
|
17
|
+
* Google-Cloud-console "OAuth client" download has, which is what OAuth-APP consumers parse
|
|
18
|
+
* (cal.com: `JSON.parse(GOOGLE_API_CREDENTIALS)?.web`, then a zod schema over
|
|
19
|
+
* client_id/client_secret/redirect_uris). This is a DIFFERENT credential species from the
|
|
20
|
+
* service-account JSON above: faking the SA shape for these names passes the consumer's
|
|
21
|
+
* "valid JSON" check and then yields `?.web === undefined`, so the integration silently
|
|
22
|
+
* degrades instead of erroring — the worst failure mode for a fake credential (Cal.com blind
|
|
23
|
+
* run, friction #4). No cryptographic material is needed: the client secret is an opaque
|
|
24
|
+
* bearer the twin accepts; identifying fields are unmistakably fake.
|
|
25
|
+
*/
|
|
26
|
+
export declare function fakeGoogleOAuthClientJson(): string;
|
|
27
|
+
/** Does this env NAME carry an INLINE Google OAUTH-CLIENT JSON (the `{"web":{...}}` shape)?
|
|
28
|
+
* Matches a GOOGLE stem combined with an OAuth-app marker: `API_CREDENTIALS` (cal.com's
|
|
29
|
+
* GOOGLE_API_CREDENTIALS — the name that motivated this), `OAUTH_*`, or `*_CLIENT_JSON` /
|
|
30
|
+
* `CLIENT_CREDENTIALS` forms. Deliberately NOT the SERVICE_ACCOUNT-marked names, NOT the
|
|
31
|
+
* GCP/GCLOUD/VERTEX stems (cloud-platform credentials are service accounts), and NOT bare
|
|
32
|
+
* GOOGLE_CREDENTIALS (ambiguous; historically ADC, i.e. SA-shaped — kept on the SA fake). */
|
|
33
|
+
export declare function isGoogleOAuthClientEnvName(name: string): boolean;
|
|
34
|
+
/** Does this env NAME carry an INLINE Google service-account JSON (the structurally-parsed
|
|
35
|
+
* kind)? Matches a Google-ish stem (GOOGLE/GCP/GCLOUD/VERTEX) combined with a JSON/
|
|
36
|
+
* credentials/service-account marker — e.g. GOOGLE_VERTEX_JSON, GCP_SERVICE_ACCOUNT_JSON,
|
|
37
|
+
* GOOGLE_APPLICATION_CREDENTIALS_JSON. Deliberately NOT GOOGLE_MAPS_API_KEY (opaque scalar),
|
|
38
|
+
* NOT GOOGLE_APPLICATION_CREDENTIALS (a FILE PATH — point it at a file whose contents
|
|
39
|
+
* come from fakeGoogleServiceAccountJson instead), and NOT the OAuth-client names above —
|
|
40
|
+
* those need the `{"web":{...}}` shape, not SA JSON. */
|
|
41
|
+
export declare function isGoogleServiceAccountEnvName(name: string): boolean;
|
|
42
|
+
export declare function fakeEnvValue(name: string): string;
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
// RSA-2048 generation costs ~100ms — mint ONCE per process and reuse. Throwaway by design: never
|
|
30
|
+
// persisted, never committed, corresponds to no real account.
|
|
31
|
+
let cachedPrivateKeyPem;
|
|
32
|
+
function throwawayPrivateKeyPem() {
|
|
33
|
+
if (!cachedPrivateKeyPem) {
|
|
34
|
+
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
35
|
+
cachedPrivateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
36
|
+
}
|
|
37
|
+
return cachedPrivateKeyPem;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A STRUCTURALLY VALID fake Google service-account JSON: every field a real
|
|
41
|
+
* `service_account` key file carries, with a REAL throwaway RSA private key — so
|
|
42
|
+
* google-auth-library (and every SDK built on it: Vertex, GCS, @ai-sdk/google-vertex, …)
|
|
43
|
+
* loads it, signs its JWT assertion locally, and proceeds to the twin-served token
|
|
44
|
+
* endpoint instead of throwing before the first request.
|
|
45
|
+
*/
|
|
46
|
+
export function fakeGoogleServiceAccountJson(opts = {}) {
|
|
47
|
+
const projectId = opts.projectId ?? 'twin-project';
|
|
48
|
+
const clientEmail = opts.clientEmail ?? `twin-fake@${projectId}.iam.gserviceaccount.com`;
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
type: 'service_account',
|
|
51
|
+
project_id: projectId,
|
|
52
|
+
private_key_id: 'twinfake0000000000000000000000000000dead',
|
|
53
|
+
private_key: throwawayPrivateKeyPem(),
|
|
54
|
+
client_email: clientEmail,
|
|
55
|
+
client_id: '000000000000000000000',
|
|
56
|
+
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
|
57
|
+
token_uri: 'https://oauth2.googleapis.com/token',
|
|
58
|
+
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
|
59
|
+
client_x509_cert_url: `https://www.googleapis.com/robot/v1/metadata/x509/${encodeURIComponent(clientEmail)}`,
|
|
60
|
+
universe_domain: 'googleapis.com',
|
|
61
|
+
}, null, 2);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A STRUCTURALLY VALID fake Google OAUTH WEB-CLIENT JSON — the `{"web":{...}}` shape a
|
|
65
|
+
* Google-Cloud-console "OAuth client" download has, which is what OAuth-APP consumers parse
|
|
66
|
+
* (cal.com: `JSON.parse(GOOGLE_API_CREDENTIALS)?.web`, then a zod schema over
|
|
67
|
+
* client_id/client_secret/redirect_uris). This is a DIFFERENT credential species from the
|
|
68
|
+
* service-account JSON above: faking the SA shape for these names passes the consumer's
|
|
69
|
+
* "valid JSON" check and then yields `?.web === undefined`, so the integration silently
|
|
70
|
+
* degrades instead of erroring — the worst failure mode for a fake credential (Cal.com blind
|
|
71
|
+
* run, friction #4). No cryptographic material is needed: the client secret is an opaque
|
|
72
|
+
* bearer the twin accepts; identifying fields are unmistakably fake.
|
|
73
|
+
*/
|
|
74
|
+
export function fakeGoogleOAuthClientJson() {
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
web: {
|
|
77
|
+
client_id: '000000000000-twinfake.apps.googleusercontent.com',
|
|
78
|
+
project_id: 'twin-project',
|
|
79
|
+
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
|
80
|
+
token_uri: 'https://oauth2.googleapis.com/token',
|
|
81
|
+
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
|
82
|
+
client_secret: 'twin-fake-google-oauth-client-secret',
|
|
83
|
+
redirect_uris: ['http://localhost:3000/api/auth/callback/google'],
|
|
84
|
+
},
|
|
85
|
+
}, null, 2);
|
|
86
|
+
}
|
|
87
|
+
/** Does this env NAME carry an INLINE Google OAUTH-CLIENT JSON (the `{"web":{...}}` shape)?
|
|
88
|
+
* Matches a GOOGLE stem combined with an OAuth-app marker: `API_CREDENTIALS` (cal.com's
|
|
89
|
+
* GOOGLE_API_CREDENTIALS — the name that motivated this), `OAUTH_*`, or `*_CLIENT_JSON` /
|
|
90
|
+
* `CLIENT_CREDENTIALS` forms. Deliberately NOT the SERVICE_ACCOUNT-marked names, NOT the
|
|
91
|
+
* GCP/GCLOUD/VERTEX stems (cloud-platform credentials are service accounts), and NOT bare
|
|
92
|
+
* GOOGLE_CREDENTIALS (ambiguous; historically ADC, i.e. SA-shaped — kept on the SA fake). */
|
|
93
|
+
export function isGoogleOAuthClientEnvName(name) {
|
|
94
|
+
const n = name.toUpperCase();
|
|
95
|
+
if (!/(^|_)GOOGLE(_|$)/.test(n))
|
|
96
|
+
return false;
|
|
97
|
+
if (n === 'GOOGLE_APPLICATION_CREDENTIALS')
|
|
98
|
+
return false; // a path, not inline JSON
|
|
99
|
+
if (/SERVICE_ACCOUNT/.test(n))
|
|
100
|
+
return false; // explicitly SA-shaped
|
|
101
|
+
return /(API_CREDENTIALS|OAUTH_(CLIENT|CREDENTIALS|JSON)|CLIENT_(JSON|CREDENTIALS))/.test(n);
|
|
102
|
+
}
|
|
103
|
+
/** Does this env NAME carry an INLINE Google service-account JSON (the structurally-parsed
|
|
104
|
+
* kind)? Matches a Google-ish stem (GOOGLE/GCP/GCLOUD/VERTEX) combined with a JSON/
|
|
105
|
+
* credentials/service-account marker — e.g. GOOGLE_VERTEX_JSON, GCP_SERVICE_ACCOUNT_JSON,
|
|
106
|
+
* GOOGLE_APPLICATION_CREDENTIALS_JSON. Deliberately NOT GOOGLE_MAPS_API_KEY (opaque scalar),
|
|
107
|
+
* NOT GOOGLE_APPLICATION_CREDENTIALS (a FILE PATH — point it at a file whose contents
|
|
108
|
+
* come from fakeGoogleServiceAccountJson instead), and NOT the OAuth-client names above —
|
|
109
|
+
* those need the `{"web":{...}}` shape, not SA JSON. */
|
|
110
|
+
export function isGoogleServiceAccountEnvName(name) {
|
|
111
|
+
const n = name.toUpperCase();
|
|
112
|
+
if (!/(^|_)(GOOGLE|GCP|GCLOUD|VERTEX)(_|$)/.test(n))
|
|
113
|
+
return false;
|
|
114
|
+
if (n === 'GOOGLE_APPLICATION_CREDENTIALS')
|
|
115
|
+
return false; // a path, not inline JSON
|
|
116
|
+
if (isGoogleOAuthClientEnvName(name))
|
|
117
|
+
return false; // OAuth-client shape, handled separately
|
|
118
|
+
return /(_JSON$|_JSON_|CREDENTIALS|SERVICE_ACCOUNT)/.test(n);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A structurally valid fake value for one env NAME. Google OAuth-client-shaped names get the
|
|
122
|
+
* `{"web":{...}}` client JSON; Google service-account-shaped names get the full SA JSON (real
|
|
123
|
+
* throwaway key); endpoint-shaped names get a parseable vendor-host URL that the injector can
|
|
124
|
+
* claim; everything else gets an unmistakably fake opaque scalar (`twin-fake-<name>`), which is
|
|
125
|
+
* sufficient for bearer-token credentials because the twin accepts any value — shape only
|
|
126
|
+
* matters when the CLIENT parses it (rule of thumb: if a blind adoption run shows an SDK throwing
|
|
127
|
+
* on a fake before any request is made, that env var needs a structural fake; add its rule here).
|
|
128
|
+
*/
|
|
129
|
+
/** Endpoint values whose SHAPE is consumed before the first network call.
|
|
130
|
+
*
|
|
131
|
+
* `new URL(value)` runs inside each client SDK before the injector can see a request, so an opaque
|
|
132
|
+
* `twin-fake-*` scalar fails even though the vendor is otherwise perfectly intercepted. These
|
|
133
|
+
* values are deliberately real-vendor-HOST-SHAPED but credential-free: the injector claims the
|
|
134
|
+
* hostname and sends the request to the local twin, while the slug remains unmistakably fake.
|
|
135
|
+
*
|
|
136
|
+
* Like CREDENTIAL_SHAPES below, this is duplicated kernel data with an explicit canonical source:
|
|
137
|
+
* world-runtime must not import a pack, and the tests pin every value against the injector's actual
|
|
138
|
+
* VENDOR_HOSTS predicate so either side changing makes the contract fail loudly. */
|
|
139
|
+
const ENDPOINT_SHAPES = [
|
|
140
|
+
{
|
|
141
|
+
match: /(^|_)UPSTASH_REDIS_REST_URL$/,
|
|
142
|
+
value: 'https://twin-fake.upstash.io',
|
|
143
|
+
source: 'packages/world-core/inject.cjs VENDOR_HOSTS.upstash',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
match: /(^|_)UPSTASH_VECTOR_REST_URL$/,
|
|
147
|
+
value: 'https://twin-fake-us1-vector.upstash.io',
|
|
148
|
+
source: 'packages/world-core/inject.cjs VENDOR_HOSTS.upstashvector',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
match: /(^|_)TINYBIRD_API_URL$/,
|
|
152
|
+
value: 'https://api.tinybird.co',
|
|
153
|
+
source: 'packages/world-core/inject.cjs VENDOR_HOSTS.tinybird',
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
/** Credentials whose SHAPE the receiving twin actually checks.
|
|
157
|
+
*
|
|
158
|
+
* The generic `twin-fake-<name>` value is inert everywhere it lands — except at a twin that
|
|
159
|
+
* validates its own vendor's key format. Then the world is fully covered and the app still gets a
|
|
160
|
+
* 401, which reads as "the twin doesn't work" and is the most confusing failure this system can
|
|
161
|
+
* produce (measured: it cost two of four subjects a clean boot in PROOF-2026-08-20.md, finding F3).
|
|
162
|
+
* A minted fake must satisfy the twin that will receive it — the minter and the twin are two halves
|
|
163
|
+
* of one contract.
|
|
164
|
+
*
|
|
165
|
+
* These literals are DUPLICATED from the packs on purpose: the kernel must not import a pack
|
|
166
|
+
* (docs/contributing/architecture.md). Each entry names its source so the pair can be re-checked by hand. */
|
|
167
|
+
const CREDENTIAL_SHAPES = [
|
|
168
|
+
// A Discord bot token is three dot-separated base64 segments; a library parses the first for the
|
|
169
|
+
// application id, so a structurally wrong fake breaks before any call reaches the twin.
|
|
170
|
+
{ 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' },
|
|
171
|
+
// resend-twin.ts enforces bearer auth of the form `re_<...>` whenever a key is presented.
|
|
172
|
+
{ match: /(^|_)RESEND_[A-Z0-9_]*(KEY|TOKEN|SECRET)$/, value: (name) => `re_twinfake_${name.toLowerCase().replace(/_/g, '')}`, source: 'packages/twin/resend/src/resend-twin.ts' },
|
|
173
|
+
// The vercel twin answers only a bearer it minted, its bootstrap token first (`vercel.platform.token_identity`); any
|
|
174
|
+
// other value is Vercel's 403 "invalid token", so every call an app makes to its Vercel project fails.
|
|
175
|
+
{ match: /(^|_)VERCEL_[A-Z0-9_]*(TOKEN|API_KEY)$/, value: () => 'vercel_twin_bootstrap_token', source: 'packages/twin/vercel/src/vercel-runtime.ts VERCEL_TWIN_TOKEN' },
|
|
176
|
+
// The tremendous twin takes a key of the shape Tremendous issues, `TEST_` for the sandbox organization at
|
|
177
|
+
// testflight.tremendous.com and `PROD_` for production (manifest.ts `auth.keyFormat`); any other value is Tremendous's
|
|
178
|
+
// 401. The `TEST_` prefix is what selects the sandbox host in clients that branch on it (dub's
|
|
179
|
+
// lib/tremendous/configuration.ts).
|
|
180
|
+
{ match: /(^|_)TREMENDOUS_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'TEST_twin_sandbox_api_key', source: 'packages/twin/tremendous/src/manifest.ts auth.keyFormat' },
|
|
181
|
+
// Clerk: the SECRET key is opaque to the twin (any sk_ shape), but the PUBLISHABLE key is parsed by
|
|
182
|
+
// clerk-js — `pk_test_<base64(frontendApiHost$)>` names the frontend-API host the browser calls.
|
|
183
|
+
// The world issues one naming the dev-instance family the clerk pack CLAIMS by suffix
|
|
184
|
+
// (`.clerk.accounts.dev`, packages/twin/clerk/src/index.ts hosts), so the boundary — the injector
|
|
185
|
+
// server-side, the browser proxy browser-side — resolves it to the twin, and an app CSP that
|
|
186
|
+
// allowlists that host stays legal (peak drive 2026-08-26).
|
|
187
|
+
{ match: /(^|_)CLERK_[A-Z0-9_]*SECRET[A-Z0-9_]*$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'packages/twin/clerk/src/clerk-twin.ts' },
|
|
188
|
+
{ match: /(^|_)CLERK_[A-Z0-9_]*PUBLISHABLE[A-Z0-9_]*$/, value: () => `pk_test_${Buffer.from('twin.clerk.accounts.dev$').toString('base64')}`, source: 'packages/twin/clerk/src/index.ts' },
|
|
189
|
+
];
|
|
190
|
+
/** Keys the APPLICATION parses before it can use them: a fake that is not the shape its own code decodes throws at the
|
|
191
|
+
* first use, far from any vendor. Each entry names the reader it satisfies; names differ in what they need (cal.com's
|
|
192
|
+
* CALENDSO_ENCRYPTION_KEY is 32 raw characters), so a rule matches the name exactly. */
|
|
193
|
+
const APP_KEY_SHAPES = [
|
|
194
|
+
// AES-256-GCM: the key base64-decodes to 32 bytes ("ENCRYPTION_KEY must be 32 bytes (base64-encoded)"); the 32 bytes
|
|
195
|
+
// spell out that they are fake
|
|
196
|
+
{ match: /^ENCRYPTION_KEY$/, value: () => Buffer.from('twin-fake-encryption-key-32bytes').toString('base64'), source: "dubinc/dub apps/web/lib/encryption.ts" },
|
|
197
|
+
];
|
|
198
|
+
export function fakeEnvValue(name) {
|
|
199
|
+
if (isGoogleOAuthClientEnvName(name))
|
|
200
|
+
return fakeGoogleOAuthClientJson();
|
|
201
|
+
for (const shape of APP_KEY_SHAPES)
|
|
202
|
+
if (shape.match.test(name))
|
|
203
|
+
return shape.value();
|
|
204
|
+
if (isGoogleServiceAccountEnvName(name))
|
|
205
|
+
return fakeGoogleServiceAccountJson();
|
|
206
|
+
for (const shape of ENDPOINT_SHAPES)
|
|
207
|
+
if (shape.match.test(name))
|
|
208
|
+
return shape.value;
|
|
209
|
+
for (const shape of CREDENTIAL_SHAPES)
|
|
210
|
+
if (shape.match.test(name))
|
|
211
|
+
return shape.value(name);
|
|
212
|
+
const slug = name.toLowerCase().replace(/_/g, '-');
|
|
213
|
+
// A URL-shaped name must be faked as a URL (F4). An SDK handed `twin-fake-x` where it expects an
|
|
214
|
+
// endpoint throws while CONSTRUCTING its client — before it ever issues the request the world
|
|
215
|
+
// could have intercepted, so the failure looks like a twin bug and no ledger entry exists to
|
|
216
|
+
// contradict that. `.invalid` is reserved (RFC 2606) and never resolves, so an untwinned vendor
|
|
217
|
+
// still fails — but as an unreachable HOST, which the diagnostics can name.
|
|
218
|
+
if (/(^|_)(URL|URI|ENDPOINT|HOST)$/.test(name))
|
|
219
|
+
return `https://${slug}.invalid`;
|
|
220
|
+
return `twin-fake-${slug}`;
|
|
221
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { keepProcessAlive } from '@volter/world-core/lifecycle';
|
|
3
|
+
// volter-world-host: run a set of twins co-located in this single process. The world
|
|
4
|
+
// runtime spawns ONE of these (instead of one bin per twin) when a world selects
|
|
5
|
+
// 'colocated'/'worker' isolation. Each twin is named by a module specifier + export so
|
|
6
|
+
// this stays vendor-agnostic (no pack imports here — dynamic import() resolves them).
|
|
7
|
+
//
|
|
8
|
+
// volter-world-host --isolation worker \
|
|
9
|
+
// --spec stripe|@volter/twin-stripe|createStripeTwinServer|12111|/data/stripe \
|
|
10
|
+
// --spec github|@volter/twin-github|createGithubTwinServer|12112|/data/github
|
|
11
|
+
//
|
|
12
|
+
// Prints one `ready id=<id> port=<port>` line per twin, then `host ready (<n> twins)`.
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { startColocatedHost } from "./host.js";
|
|
16
|
+
const argv = process.argv.slice(2);
|
|
17
|
+
const opt = (name, fb = '') => { const i = argv.indexOf(name); return i >= 0 && argv[i + 1] ? argv[i + 1] : fb; };
|
|
18
|
+
const isolation = opt('--isolation', 'shared');
|
|
19
|
+
if (isolation !== 'shared' && isolation !== 'worker') {
|
|
20
|
+
process.stderr.write(`invalid --isolation "${isolation}" (want shared|worker)\n`);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
}
|
|
23
|
+
const specs = [];
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
if (argv[i] !== '--spec')
|
|
26
|
+
continue;
|
|
27
|
+
const raw = argv[i + 1] ?? '';
|
|
28
|
+
const [id, module, exportName, port, root, ...flags] = raw.split('|');
|
|
29
|
+
const ro = flags.find((f) => f === 'ro');
|
|
30
|
+
const scenarioPath = flags.find((f) => f.startsWith('scenario='))?.slice('scenario='.length);
|
|
31
|
+
if (!id || !module || !exportName || !port || !root) {
|
|
32
|
+
process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>])\n`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}) });
|
|
36
|
+
}
|
|
37
|
+
if (specs.length === 0) {
|
|
38
|
+
process.stderr.write('no --spec given\n');
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
// A worker give-up (host.ts fires it on the first crash — never restarted) must be discoverable
|
|
42
|
+
// AFTER the fact, not just as a
|
|
43
|
+
// stdout line — this process IS the world's `instance.json` host child, but it is a SEPARATE OS
|
|
44
|
+
// process from the runtime that wrote instance.json, so it must not rewrite that file directly
|
|
45
|
+
// (the parent may still be mid-write, or rewrite it later — share/unshare both call
|
|
46
|
+
// saveWorldInstance). Instead it writes a small sidecar (`host-gaveup.json`) next to instance.json
|
|
47
|
+
// in the SAME instance dir (already created by upWorld before this child is spawned); `readWorldInstance`
|
|
48
|
+
// merges the sidecar into `services[<id>].workerGaveUp` so every reader (doctor, status, tests) sees
|
|
49
|
+
// the record as if it were in instance.json, without any cross-process write contention.
|
|
50
|
+
// The instance file path is threaded in via VOLTER_WORLD_INSTANCE (set by upWorld for every co-located
|
|
51
|
+
// child). Absent it (e.g. this CLI driven standalone in a unit test), give-up persistence is a no-op —
|
|
52
|
+
// host.test.ts covers shared mode in-process; the give-up mechanics themselves (worker crash →
|
|
53
|
+
// `gaveup` event, never respawned) are covered by the host-isolation / colocated-world integration
|
|
54
|
+
// tests, which spawn this CLI and observe it from the outside.
|
|
55
|
+
const instanceFile = process.env.VOLTER_WORLD_INSTANCE;
|
|
56
|
+
const gaveUpSidecar = instanceFile ? join(dirname(instanceFile), 'host-gaveup.json') : undefined;
|
|
57
|
+
function recordGaveUp(twin, exits, detail) {
|
|
58
|
+
if (!gaveUpSidecar)
|
|
59
|
+
return;
|
|
60
|
+
let all = {};
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(gaveUpSidecar))
|
|
63
|
+
all = JSON.parse(readFileSync(gaveUpSidecar, 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// A torn/partial read (e.g. we crashed mid-write previously) — start fresh rather than
|
|
67
|
+
// let a corrupt sidecar wedge future give-ups from ever being recorded.
|
|
68
|
+
}
|
|
69
|
+
all[twin] = { at: new Date().toISOString(), exits, detail };
|
|
70
|
+
// Atomic publish (temp→rename in the SAME dir), the same discipline the kernel/instance.json
|
|
71
|
+
// writers use — a reader never observes a partially-written sidecar.
|
|
72
|
+
mkdirSync(dirname(gaveUpSidecar), { recursive: true });
|
|
73
|
+
const tmp = `${gaveUpSidecar}.${process.pid}.tmp`;
|
|
74
|
+
writeFileSync(tmp, `${JSON.stringify(all, null, 2)}\n`);
|
|
75
|
+
renameSync(tmp, gaveUpSidecar);
|
|
76
|
+
}
|
|
77
|
+
// deploy: auto is the kernel's head (core head.ts): a twin performs its own `auto` writes as they happen
|
|
78
|
+
const host = await startColocatedHost(specs, {
|
|
79
|
+
isolation,
|
|
80
|
+
onEvent: (e) => {
|
|
81
|
+
process.stdout.write(`event twin=${e.twin} kind=${e.kind}${e.detail ? ` detail=${e.detail}` : ''}\n`);
|
|
82
|
+
if (e.kind === 'gaveup')
|
|
83
|
+
recordGaveUp(e.twin, e.exits ?? 0, e.detail ?? '');
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
for (const [id, port] of Object.entries(host.ports))
|
|
87
|
+
process.stdout.write(`ready id=${id} port=${port}\n`);
|
|
88
|
+
process.stdout.write(`host ready (${specs.length} twins, isolation=${isolation})\n`);
|
|
89
|
+
const shutdown = async () => { await host.stop(); process.exit(0); };
|
|
90
|
+
process.on('SIGTERM', shutdown);
|
|
91
|
+
process.on('SIGINT', shutdown);
|
|
92
|
+
await keepProcessAlive();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type TwinServer = {
|
|
2
|
+
port: number;
|
|
3
|
+
stop: () => void;
|
|
4
|
+
};
|
|
5
|
+
export declare function createFaultTwinServer(options?: {
|
|
6
|
+
port?: number;
|
|
7
|
+
root?: string;
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
}): TwinServer;
|
|
10
|
+
export declare const stopsInvoked: string[];
|
|
11
|
+
/** ASYNC factory (the fly shape): the host must await it or ports/stop are a Promise. */
|
|
12
|
+
export declare function createAsyncFaultTwinServer(options?: {
|
|
13
|
+
port?: number;
|
|
14
|
+
root?: string;
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
}): Promise<TwinServer>;
|
|
17
|
+
/** stop() never resolves — the hang outcome. */
|
|
18
|
+
export declare function createPendingStopTwinServer(options?: {
|
|
19
|
+
port?: number;
|
|
20
|
+
root?: string;
|
|
21
|
+
}): TwinServer;
|
|
22
|
+
/** stop() rejects — the throwing outcome (fly's failed container removal). */
|
|
23
|
+
export declare function createRejectingStopTwinServer(options?: {
|
|
24
|
+
port?: number;
|
|
25
|
+
root?: string;
|
|
26
|
+
}): TwinServer;
|
|
27
|
+
/** well-behaved async stop that RECORDS being invoked. */
|
|
28
|
+
export declare function createRecordingStopTwinServer(options?: {
|
|
29
|
+
port?: number;
|
|
30
|
+
root?: string;
|
|
31
|
+
}): TwinServer;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// TEST-ONLY fault-injection twin (excluded from the published package via package.json
|
|
2
|
+
// `files`). It is the in-process analog of the runtime suite's inline `node -e` crash
|
|
3
|
+
// fixtures: a real `({port,root,readOnly}) => {port,stop}` twin factory whose handler can
|
|
4
|
+
// be told to fail in a specific way by path, so the co-located host's isolation contract
|
|
5
|
+
// can be proven per tier (shared vs worker):
|
|
6
|
+
// GET /ok → 200 (the neighbor/health endpoint)
|
|
7
|
+
// GET /count → 200 n (root-keyed counter — proves state isolation)
|
|
8
|
+
// POST /fault/throw → handler throws (contained by Bun.serve → 500; process survives)
|
|
9
|
+
// GET /fault/spin → busy-loops ?ms (blocks the event loop — shared: stalls siblings)
|
|
10
|
+
// POST /fault/reject → stray async rejection (tests the shared rejection guard)
|
|
11
|
+
// POST /fault/exit → process.exit(1) (shared: kills host; worker: kills only that worker)
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
export function createFaultTwinServer(options = {}) {
|
|
15
|
+
const root = options.root;
|
|
16
|
+
const countFile = root ? join(root, 'count') : undefined;
|
|
17
|
+
if (root)
|
|
18
|
+
mkdirSync(root, { recursive: true });
|
|
19
|
+
const bump = () => {
|
|
20
|
+
if (!countFile)
|
|
21
|
+
return -1;
|
|
22
|
+
const n = (existsSync(countFile) ? Number(readFileSync(countFile, 'utf8')) || 0 : 0) + 1;
|
|
23
|
+
const tmp = `${countFile}.${process.pid}.${n}.tmp`;
|
|
24
|
+
writeFileSync(tmp, String(n));
|
|
25
|
+
// atomic publish (temp→rename), the same discipline the kernel uses
|
|
26
|
+
renameSync(tmp, countFile);
|
|
27
|
+
return n;
|
|
28
|
+
};
|
|
29
|
+
const server = Bun.serve({
|
|
30
|
+
hostname: '127.0.0.1',
|
|
31
|
+
port: options.port ?? 0,
|
|
32
|
+
idleTimeout: 30,
|
|
33
|
+
async fetch(request) {
|
|
34
|
+
const url = new URL(request.url);
|
|
35
|
+
const p = url.pathname;
|
|
36
|
+
if (p === '/ok' || p === '/')
|
|
37
|
+
return new Response('ok', { status: 200 });
|
|
38
|
+
if (p === '/count')
|
|
39
|
+
return new Response(String(bump()), { status: 200 });
|
|
40
|
+
if (p === '/fault/throw')
|
|
41
|
+
throw new Error('injected fault: throw');
|
|
42
|
+
if (p === '/fault/spin') {
|
|
43
|
+
const ms = Number(url.searchParams.get('ms') ?? '1000');
|
|
44
|
+
const until = Date.now() + ms;
|
|
45
|
+
while (Date.now() < until) { /* busy-loop: hog the event loop */ }
|
|
46
|
+
return new Response('spun', { status: 200 });
|
|
47
|
+
}
|
|
48
|
+
if (p === '/fault/reject') {
|
|
49
|
+
queueMicrotask(() => { void Promise.reject(new Error('injected fault: stray rejection')); });
|
|
50
|
+
return new Response('scheduled', { status: 202 });
|
|
51
|
+
}
|
|
52
|
+
if (p === '/fault/exit') {
|
|
53
|
+
// Defer so the response flushes, then hard-exit this process/worker.
|
|
54
|
+
setTimeout(() => process.exit(1), 5);
|
|
55
|
+
return new Response('exiting', { status: 200 });
|
|
56
|
+
}
|
|
57
|
+
return new Response('not found', { status: 404 });
|
|
58
|
+
},
|
|
59
|
+
// A real vendor returns 500 on an internal error (not a dropped socket). Surfacing the
|
|
60
|
+
// throw as a clean 500 is both faithful AND the request-level isolation boundary: the
|
|
61
|
+
// throw never escapes this handler to disturb a sibling twin.
|
|
62
|
+
error() {
|
|
63
|
+
return new Response('internal error', { status: 500 });
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
if (root)
|
|
68
|
+
writeFileSync(join(root, 'listening-port'), String(server.port));
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
server.stop(true);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
75
|
+
}
|
|
76
|
+
// ── shutdown-contract fixtures (§9 round two H2/H3, 2026-08-31) ────────────────────────────
|
|
77
|
+
// The mount/stop contract admits async factories and async stops, and host.stop() must
|
|
78
|
+
// survive every stop OUTCOME: these factories make each outcome constructible. `stopsInvoked`
|
|
79
|
+
// proves "every stop is invoked" independently of what the stops then do.
|
|
80
|
+
export const stopsInvoked = [];
|
|
81
|
+
/** ASYNC factory (the fly shape): the host must await it or ports/stop are a Promise. */
|
|
82
|
+
export async function createAsyncFaultTwinServer(options = {}) {
|
|
83
|
+
await new Promise((tick) => setTimeout(tick, 10));
|
|
84
|
+
return createFaultTwinServer(options);
|
|
85
|
+
}
|
|
86
|
+
/** stop() never resolves — the hang outcome. */
|
|
87
|
+
export function createPendingStopTwinServer(options = {}) {
|
|
88
|
+
const inner = createFaultTwinServer(options);
|
|
89
|
+
return { port: inner.port, stop: () => { stopsInvoked.push('pending'); void inner.stop(); return new Promise(() => { }); } };
|
|
90
|
+
}
|
|
91
|
+
/** stop() rejects — the throwing outcome (fly's failed container removal). */
|
|
92
|
+
export function createRejectingStopTwinServer(options = {}) {
|
|
93
|
+
const inner = createFaultTwinServer(options);
|
|
94
|
+
return { port: inner.port, stop: () => { stopsInvoked.push('rejecting'); void inner.stop(); return Promise.reject(new Error('injected fault: stop rejected')); } };
|
|
95
|
+
}
|
|
96
|
+
/** well-behaved async stop that RECORDS being invoked. */
|
|
97
|
+
export function createRecordingStopTwinServer(options = {}) {
|
|
98
|
+
const inner = createFaultTwinServer(options);
|
|
99
|
+
return { port: inner.port, stop: async () => { stopsInvoked.push('recording'); await inner.stop(); } };
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// Worker entry for 'worker' isolation: one Worker thread per twin. It owns its event
|
|
10
|
+
// loop + heap, so a CPU hog / leak / hard crash here is contained to this thread — the
|
|
11
|
+
// host does NOT restart it: it gives up on the worker (fires a `gaveup` event → doctor
|
|
12
|
+
// red) without disturbing sibling twins. Posts {type:'ready'} once listening.
|
|
13
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
14
|
+
const spec = workerData;
|
|
15
|
+
const mod = (await import(__rewriteRelativeImportExtension(spec.module)));
|
|
16
|
+
const factory = mod[spec.export];
|
|
17
|
+
if (typeof factory !== 'function') {
|
|
18
|
+
throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
|
|
19
|
+
}
|
|
20
|
+
// Awaited: an async factory (fly) must post `ready` only once its listener is bound.
|
|
21
|
+
const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly });
|
|
22
|
+
parentPort?.postMessage({ type: 'ready', port: server.port });
|
|
23
|
+
// The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** One twin to mount in the host. `module`/`export` name a `({port,root,readOnly}) => {port,stop}`
|
|
2
|
+
* factory (every pack ships one, e.g. `createStripeTwinServer`). `module` is anything import()
|
|
3
|
+
* resolves: a package name (`@volter/twin-stripe`) or an absolute file path (tests/fixtures). */
|
|
4
|
+
export type ColocatedTwinSpec = {
|
|
5
|
+
id: string;
|
|
6
|
+
module: string;
|
|
7
|
+
export: string;
|
|
8
|
+
port: number;
|
|
9
|
+
root: string;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
/** The world's handlers file for a scenario-carrying (generative) twin — the colocate contract's
|
|
12
|
+
* scenarioPath (R2a): the same file the spawn path passes as --scenario. */
|
|
13
|
+
scenarioPath?: string;
|
|
14
|
+
};
|
|
15
|
+
export type HostIsolation = 'shared' | 'worker';
|
|
16
|
+
export type ColocatedHost = {
|
|
17
|
+
isolation: HostIsolation;
|
|
18
|
+
/** Resolved listening port per twin id (equals the requested port). */
|
|
19
|
+
ports: Record<string, number>;
|
|
20
|
+
stop: () => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Start a co-located host. Resolves once every twin is listening. The returned `stop()`
|
|
24
|
+
* tears the whole host down (all servers / all workers).
|
|
25
|
+
*
|
|
26
|
+
* `onEvent` is an optional observability hook (logged by the CLI) — it surfaces a twin
|
|
27
|
+
* worker crashing (and being given up on), and stray rejections swallowed in shared mode,
|
|
28
|
+
* so a degraded twin is never a silent fake-success.
|
|
29
|
+
*/
|
|
30
|
+
export declare function startColocatedHost(specs: ColocatedTwinSpec[], options?: {
|
|
31
|
+
isolation?: HostIsolation;
|
|
32
|
+
onEvent?: (e: {
|
|
33
|
+
twin: string;
|
|
34
|
+
kind: string;
|
|
35
|
+
detail?: string;
|
|
36
|
+
exits?: number;
|
|
37
|
+
}) => void;
|
|
38
|
+
}): Promise<ColocatedHost>;
|