@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.
Files changed (164) hide show
  1. package/LICENSE +202 -0
  2. package/dist/known-external-services.json +1108 -0
  3. package/dist/src/ancestry.d.ts +2 -0
  4. package/dist/src/ancestry.js +42 -0
  5. package/dist/src/app-url.d.ts +47 -0
  6. package/dist/src/app-url.js +239 -0
  7. package/dist/src/attach.d.ts +48 -0
  8. package/dist/src/attach.js +87 -0
  9. package/dist/src/branch.d.ts +20 -0
  10. package/dist/src/branch.js +65 -0
  11. package/dist/src/browser-proxy-cli.d.ts +2 -0
  12. package/dist/src/browser-proxy-cli.js +41 -0
  13. package/dist/src/ca-trust.d.ts +5 -0
  14. package/dist/src/ca-trust.js +64 -0
  15. package/dist/src/catalog.d.ts +31 -0
  16. package/dist/src/catalog.js +148 -0
  17. package/dist/src/changeset.d.ts +142 -0
  18. package/dist/src/changeset.js +570 -0
  19. package/dist/src/cli.d.ts +2 -0
  20. package/dist/src/cli.js +1262 -0
  21. package/dist/src/command-lifetime.d.ts +15 -0
  22. package/dist/src/command-lifetime.js +98 -0
  23. package/dist/src/configs.d.ts +18 -0
  24. package/dist/src/configs.js +119 -0
  25. package/dist/src/console-apart.d.ts +38 -0
  26. package/dist/src/console-apart.js +107 -0
  27. package/dist/src/consumers.d.ts +46 -0
  28. package/dist/src/consumers.js +200 -0
  29. package/dist/src/covers.d.ts +183 -0
  30. package/dist/src/covers.js +800 -0
  31. package/dist/src/fixture-env.d.ts +42 -0
  32. package/dist/src/fixture-env.js +221 -0
  33. package/dist/src/host-cli.d.ts +2 -0
  34. package/dist/src/host-cli.js +92 -0
  35. package/dist/src/host-fault-fixture.d.ts +32 -0
  36. package/dist/src/host-fault-fixture.js +100 -0
  37. package/dist/src/host-worker.d.ts +1 -0
  38. package/dist/src/host-worker.js +23 -0
  39. package/dist/src/host.d.ts +38 -0
  40. package/dist/src/host.js +135 -0
  41. package/dist/src/index.d.ts +48 -0
  42. package/dist/src/index.js +35 -0
  43. package/dist/src/infra-cli.d.ts +2 -0
  44. package/dist/src/infra-cli.js +136 -0
  45. package/dist/src/init.d.ts +227 -0
  46. package/dist/src/init.js +1117 -0
  47. package/dist/src/inject-map.d.ts +34 -0
  48. package/dist/src/inject-map.js +56 -0
  49. package/dist/src/lifecycle-record.d.ts +47 -0
  50. package/dist/src/lifecycle-record.js +196 -0
  51. package/dist/src/origin.d.ts +31 -0
  52. package/dist/src/origin.js +139 -0
  53. package/dist/src/pack-facts.d.ts +75 -0
  54. package/dist/src/pack-facts.js +98 -0
  55. package/dist/src/pglite-backing.d.ts +21 -0
  56. package/dist/src/pglite-backing.js +158 -0
  57. package/dist/src/pglite-host.mjs +147 -0
  58. package/dist/src/placeholder.d.ts +20 -0
  59. package/dist/src/placeholder.js +100 -0
  60. package/dist/src/prerequisites.d.ts +21 -0
  61. package/dist/src/prerequisites.js +49 -0
  62. package/dist/src/process-groups.d.ts +4 -0
  63. package/dist/src/process-groups.js +49 -0
  64. package/dist/src/project-inspect.d.ts +109 -0
  65. package/dist/src/project-inspect.js +827 -0
  66. package/dist/src/proxy-daemon.d.ts +2 -0
  67. package/dist/src/proxy-daemon.js +18 -0
  68. package/dist/src/redirect-proxy.d.ts +105 -0
  69. package/dist/src/redirect-proxy.js +665 -0
  70. package/dist/src/reflect.d.ts +74 -0
  71. package/dist/src/reflect.js +392 -0
  72. package/dist/src/resources.d.ts +26 -0
  73. package/dist/src/resources.js +22 -0
  74. package/dist/src/root.d.ts +114 -0
  75. package/dist/src/root.js +312 -0
  76. package/dist/src/run-task-worker.d.ts +1 -0
  77. package/dist/src/run-task-worker.js +38 -0
  78. package/dist/src/run-task.d.ts +18 -0
  79. package/dist/src/run-task.js +48 -0
  80. package/dist/src/runtime-test-support.d.ts +59 -0
  81. package/dist/src/runtime-test-support.js +205 -0
  82. package/dist/src/runtime.d.ts +256 -0
  83. package/dist/src/runtime.js +3502 -0
  84. package/dist/src/schema.d.ts +449 -0
  85. package/dist/src/schema.js +605 -0
  86. package/dist/src/serve.d.ts +30 -0
  87. package/dist/src/serve.js +82 -0
  88. package/dist/src/served-world.d.ts +194 -0
  89. package/dist/src/served-world.js +986 -0
  90. package/dist/src/service-exit.d.ts +46 -0
  91. package/dist/src/service-exit.js +195 -0
  92. package/dist/src/service-recorder.d.ts +1 -0
  93. package/dist/src/service-recorder.js +121 -0
  94. package/dist/src/sibling.d.ts +1 -0
  95. package/dist/src/sibling.js +9 -0
  96. package/dist/src/signals.d.ts +1 -0
  97. package/dist/src/signals.js +11 -0
  98. package/dist/src/storage-capacity.d.ts +8 -0
  99. package/dist/src/storage-capacity.js +61 -0
  100. package/dist/src/tail.d.ts +30 -0
  101. package/dist/src/tail.js +160 -0
  102. package/dist/src/tcp-port.d.ts +2 -0
  103. package/dist/src/tcp-port.js +36 -0
  104. package/dist/src/up-task-worker.d.ts +1 -0
  105. package/dist/src/up-task-worker.js +61 -0
  106. package/dist/src/up-task.d.ts +17 -0
  107. package/dist/src/up-task.js +49 -0
  108. package/dist/src/websocket-relay.d.ts +3 -0
  109. package/dist/src/websocket-relay.js +40 -0
  110. package/known-external-services.json +1108 -0
  111. package/package.json +83 -0
  112. package/src/ancestry.ts +36 -0
  113. package/src/app-url.ts +253 -0
  114. package/src/attach.ts +117 -0
  115. package/src/branch.ts +63 -0
  116. package/src/browser-proxy-cli.ts +44 -0
  117. package/src/ca-trust.ts +57 -0
  118. package/src/catalog.ts +156 -0
  119. package/src/changeset.ts +627 -0
  120. package/src/cli.ts +1111 -0
  121. package/src/command-lifetime.ts +79 -0
  122. package/src/configs.ts +110 -0
  123. package/src/console-apart.ts +90 -0
  124. package/src/consumers.ts +185 -0
  125. package/src/covers.ts +934 -0
  126. package/src/fixture-env.ts +230 -0
  127. package/src/host-cli.ts +90 -0
  128. package/src/host-worker.ts +23 -0
  129. package/src/host.ts +169 -0
  130. package/src/index.ts +171 -0
  131. package/src/infra-cli.ts +133 -0
  132. package/src/init.ts +1316 -0
  133. package/src/inject-map.ts +72 -0
  134. package/src/lifecycle-record.ts +168 -0
  135. package/src/origin.ts +134 -0
  136. package/src/pack-facts.ts +128 -0
  137. package/src/pglite-backing.ts +141 -0
  138. package/src/pglite-host.mjs +147 -0
  139. package/src/placeholder.ts +89 -0
  140. package/src/prerequisites.ts +66 -0
  141. package/src/process-groups.ts +33 -0
  142. package/src/project-inspect.ts +770 -0
  143. package/src/proxy-daemon.ts +21 -0
  144. package/src/redirect-proxy.ts +684 -0
  145. package/src/reflect.ts +440 -0
  146. package/src/resources.ts +22 -0
  147. package/src/root.ts +290 -0
  148. package/src/run-task-worker.ts +27 -0
  149. package/src/run-task.ts +44 -0
  150. package/src/runtime-test-support.ts +208 -0
  151. package/src/runtime.ts +3357 -0
  152. package/src/schema.ts +922 -0
  153. package/src/serve.ts +102 -0
  154. package/src/served-world.ts +812 -0
  155. package/src/service-exit.ts +175 -0
  156. package/src/service-recorder.ts +89 -0
  157. package/src/sibling.ts +10 -0
  158. package/src/signals.ts +10 -0
  159. package/src/storage-capacity.ts +60 -0
  160. package/src/tail.ts +205 -0
  161. package/src/tcp-port.ts +35 -0
  162. package/src/up-task-worker.ts +40 -0
  163. package/src/up-task.ts +45 -0
  164. package/src/websocket-relay.ts +32 -0
@@ -0,0 +1,230 @@
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. Throwaway by design: never
38
+ // persisted, never committed, corresponds to no real account.
39
+ let cachedPrivateKeyPem: string | undefined;
40
+ function throwawayPrivateKeyPem(): string {
41
+ if (!cachedPrivateKeyPem) {
42
+ const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
43
+ cachedPrivateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
44
+ }
45
+ return cachedPrivateKeyPem;
46
+ }
47
+
48
+ /**
49
+ * A STRUCTURALLY VALID fake Google service-account JSON: every field a real
50
+ * `service_account` key file carries, with a REAL throwaway RSA private key — so
51
+ * google-auth-library (and every SDK built on it: Vertex, GCS, @ai-sdk/google-vertex, …)
52
+ * loads it, signs its JWT assertion locally, and proceeds to the twin-served token
53
+ * endpoint instead of throwing before the first request.
54
+ */
55
+ export function fakeGoogleServiceAccountJson(opts: FakeServiceAccountOptions = {}): string {
56
+ const projectId = opts.projectId ?? 'twin-project';
57
+ const clientEmail = opts.clientEmail ?? `twin-fake@${projectId}.iam.gserviceaccount.com`;
58
+ return JSON.stringify(
59
+ {
60
+ type: 'service_account',
61
+ project_id: projectId,
62
+ private_key_id: 'twinfake0000000000000000000000000000dead',
63
+ private_key: throwawayPrivateKeyPem(),
64
+ client_email: clientEmail,
65
+ client_id: '000000000000000000000',
66
+ auth_uri: 'https://accounts.google.com/o/oauth2/auth',
67
+ token_uri: 'https://oauth2.googleapis.com/token',
68
+ auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
69
+ client_x509_cert_url: `https://www.googleapis.com/robot/v1/metadata/x509/${encodeURIComponent(clientEmail)}`,
70
+ universe_domain: 'googleapis.com',
71
+ },
72
+ null,
73
+ 2,
74
+ );
75
+ }
76
+
77
+ /**
78
+ * A STRUCTURALLY VALID fake Google OAUTH WEB-CLIENT JSON — the `{"web":{...}}` shape a
79
+ * Google-Cloud-console "OAuth client" download has, which is what OAuth-APP consumers parse
80
+ * (cal.com: `JSON.parse(GOOGLE_API_CREDENTIALS)?.web`, then a zod schema over
81
+ * client_id/client_secret/redirect_uris). This is a DIFFERENT credential species from the
82
+ * service-account JSON above: faking the SA shape for these names passes the consumer's
83
+ * "valid JSON" check and then yields `?.web === undefined`, so the integration silently
84
+ * degrades instead of erroring — the worst failure mode for a fake credential (Cal.com blind
85
+ * run, friction #4). No cryptographic material is needed: the client secret is an opaque
86
+ * bearer the twin accepts; identifying fields are unmistakably fake.
87
+ */
88
+ export function fakeGoogleOAuthClientJson(): string {
89
+ return JSON.stringify(
90
+ {
91
+ web: {
92
+ client_id: '000000000000-twinfake.apps.googleusercontent.com',
93
+ project_id: 'twin-project',
94
+ auth_uri: 'https://accounts.google.com/o/oauth2/auth',
95
+ token_uri: 'https://oauth2.googleapis.com/token',
96
+ auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
97
+ client_secret: 'twin-fake-google-oauth-client-secret',
98
+ redirect_uris: ['http://localhost:3000/api/auth/callback/google'],
99
+ },
100
+ },
101
+ null,
102
+ 2,
103
+ );
104
+ }
105
+
106
+ /** Does this env NAME carry an INLINE Google OAUTH-CLIENT JSON (the `{"web":{...}}` shape)?
107
+ * Matches a GOOGLE stem combined with an OAuth-app marker: `API_CREDENTIALS` (cal.com's
108
+ * GOOGLE_API_CREDENTIALS — the name that motivated this), `OAUTH_*`, or `*_CLIENT_JSON` /
109
+ * `CLIENT_CREDENTIALS` forms. Deliberately NOT the SERVICE_ACCOUNT-marked names, NOT the
110
+ * GCP/GCLOUD/VERTEX stems (cloud-platform credentials are service accounts), and NOT bare
111
+ * GOOGLE_CREDENTIALS (ambiguous; historically ADC, i.e. SA-shaped — kept on the SA fake). */
112
+ export function isGoogleOAuthClientEnvName(name: string): boolean {
113
+ const n = name.toUpperCase();
114
+ if (!/(^|_)GOOGLE(_|$)/.test(n)) return false;
115
+ if (n === 'GOOGLE_APPLICATION_CREDENTIALS') return false; // a path, not inline JSON
116
+ if (/SERVICE_ACCOUNT/.test(n)) return false; // explicitly SA-shaped
117
+ return /(API_CREDENTIALS|OAUTH_(CLIENT|CREDENTIALS|JSON)|CLIENT_(JSON|CREDENTIALS))/.test(n);
118
+ }
119
+
120
+ /** Does this env NAME carry an INLINE Google service-account JSON (the structurally-parsed
121
+ * kind)? Matches a Google-ish stem (GOOGLE/GCP/GCLOUD/VERTEX) combined with a JSON/
122
+ * credentials/service-account marker — e.g. GOOGLE_VERTEX_JSON, GCP_SERVICE_ACCOUNT_JSON,
123
+ * GOOGLE_APPLICATION_CREDENTIALS_JSON. Deliberately NOT GOOGLE_MAPS_API_KEY (opaque scalar),
124
+ * NOT GOOGLE_APPLICATION_CREDENTIALS (a FILE PATH — point it at a file whose contents
125
+ * come from fakeGoogleServiceAccountJson instead), and NOT the OAuth-client names above —
126
+ * those need the `{"web":{...}}` shape, not SA JSON. */
127
+ export function isGoogleServiceAccountEnvName(name: string): boolean {
128
+ const n = name.toUpperCase();
129
+ if (!/(^|_)(GOOGLE|GCP|GCLOUD|VERTEX)(_|$)/.test(n)) return false;
130
+ if (n === 'GOOGLE_APPLICATION_CREDENTIALS') return false; // a path, not inline JSON
131
+ if (isGoogleOAuthClientEnvName(name)) return false; // OAuth-client shape, handled separately
132
+ return /(_JSON$|_JSON_|CREDENTIALS|SERVICE_ACCOUNT)/.test(n);
133
+ }
134
+
135
+ /**
136
+ * A structurally valid fake value for one env NAME. Google OAuth-client-shaped names get the
137
+ * `{"web":{...}}` client JSON; Google service-account-shaped names get the full SA JSON (real
138
+ * throwaway key); endpoint-shaped names get a parseable vendor-host URL that the injector can
139
+ * claim; everything else gets an unmistakably fake opaque scalar (`twin-fake-<name>`), which is
140
+ * sufficient for bearer-token credentials because the twin accepts any value — shape only
141
+ * matters when the CLIENT parses it (rule of thumb: if a blind adoption run shows an SDK throwing
142
+ * on a fake before any request is made, that env var needs a structural fake; add its rule here).
143
+ */
144
+ /** Endpoint values whose SHAPE is consumed before the first network call.
145
+ *
146
+ * `new URL(value)` runs inside each client SDK before the injector can see a request, so an opaque
147
+ * `twin-fake-*` scalar fails even though the vendor is otherwise perfectly intercepted. These
148
+ * values are deliberately real-vendor-HOST-SHAPED but credential-free: the injector claims the
149
+ * hostname and sends the request to the local twin, while the slug remains unmistakably fake.
150
+ *
151
+ * Like CREDENTIAL_SHAPES below, this is duplicated kernel data with an explicit canonical source:
152
+ * world-runtime must not import a pack, and the tests pin every value against the injector's actual
153
+ * VENDOR_HOSTS predicate so either side changing makes the contract fail loudly. */
154
+ const ENDPOINT_SHAPES: Array<{ match: RegExp; value: string; source: string }> = [
155
+ {
156
+ match: /(^|_)UPSTASH_REDIS_REST_URL$/,
157
+ value: 'https://twin-fake.upstash.io',
158
+ source: 'packages/world-core/inject.cjs VENDOR_HOSTS.upstash',
159
+ },
160
+ {
161
+ match: /(^|_)UPSTASH_VECTOR_REST_URL$/,
162
+ value: 'https://twin-fake-us1-vector.upstash.io',
163
+ source: 'packages/world-core/inject.cjs VENDOR_HOSTS.upstashvector',
164
+ },
165
+ {
166
+ match: /(^|_)TINYBIRD_API_URL$/,
167
+ value: 'https://api.tinybird.co',
168
+ source: 'packages/world-core/inject.cjs VENDOR_HOSTS.tinybird',
169
+ },
170
+ ];
171
+
172
+ /** Credentials whose SHAPE the receiving twin actually checks.
173
+ *
174
+ * The generic `twin-fake-<name>` value is inert everywhere it lands — except at a twin that
175
+ * validates its own vendor's key format. Then the world is fully covered and the app still gets a
176
+ * 401, which reads as "the twin doesn't work" and is the most confusing failure this system can
177
+ * produce (measured: it cost two of four subjects a clean boot in PROOF-2026-08-20.md, finding F3).
178
+ * A minted fake must satisfy the twin that will receive it — the minter and the twin are two halves
179
+ * of one contract.
180
+ *
181
+ * These literals are DUPLICATED from the packs on purpose: the kernel must not import a pack
182
+ * (docs/contributing/architecture.md). Each entry names its source so the pair can be re-checked by hand. */
183
+ const CREDENTIAL_SHAPES: Array<{ match: RegExp; value: (name: string) => string; source: string }> = [
184
+ // A Discord bot token is three dot-separated base64 segments; a library parses the first for the
185
+ // application id, so a structurally wrong fake breaks before any call reaches the twin.
186
+ { 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' },
187
+ // resend-twin.ts enforces bearer auth of the form `re_<...>` whenever a key is presented.
188
+ { match: /(^|_)RESEND_[A-Z0-9_]*(KEY|TOKEN|SECRET)$/, value: (name) => `re_twinfake_${name.toLowerCase().replace(/_/g, '')}`, source: 'packages/twin/resend/src/resend-twin.ts' },
189
+ // The vercel twin answers only a bearer it minted, its bootstrap token first (`vercel.platform.token_identity`); any
190
+ // other value is Vercel's 403 "invalid token", so every call an app makes to its Vercel project fails.
191
+ { match: /(^|_)VERCEL_[A-Z0-9_]*(TOKEN|API_KEY)$/, value: () => 'vercel_twin_bootstrap_token', source: 'packages/twin/vercel/src/vercel-runtime.ts VERCEL_TWIN_TOKEN' },
192
+ // The tremendous twin takes a key of the shape Tremendous issues, `TEST_` for the sandbox organization at
193
+ // testflight.tremendous.com and `PROD_` for production (manifest.ts `auth.keyFormat`); any other value is Tremendous's
194
+ // 401. The `TEST_` prefix is what selects the sandbox host in clients that branch on it (dub's
195
+ // lib/tremendous/configuration.ts).
196
+ { match: /(^|_)TREMENDOUS_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'TEST_twin_sandbox_api_key', source: 'packages/twin/tremendous/src/manifest.ts auth.keyFormat' },
197
+ // Clerk: the SECRET key is opaque to the twin (any sk_ shape), but the PUBLISHABLE key is parsed by
198
+ // clerk-js — `pk_test_<base64(frontendApiHost$)>` names the frontend-API host the browser calls.
199
+ // The world issues one naming the dev-instance family the clerk pack CLAIMS by suffix
200
+ // (`.clerk.accounts.dev`, packages/twin/clerk/src/index.ts hosts), so the boundary — the injector
201
+ // server-side, the browser proxy browser-side — resolves it to the twin, and an app CSP that
202
+ // allowlists that host stays legal (peak drive 2026-08-26).
203
+ { match: /(^|_)CLERK_[A-Z0-9_]*SECRET[A-Z0-9_]*$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'packages/twin/clerk/src/clerk-twin.ts' },
204
+ { 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' },
205
+ ];
206
+
207
+ /** Keys the APPLICATION parses before it can use them: a fake that is not the shape its own code decodes throws at the
208
+ * first use, far from any vendor. Each entry names the reader it satisfies; names differ in what they need (cal.com's
209
+ * CALENDSO_ENCRYPTION_KEY is 32 raw characters), so a rule matches the name exactly. */
210
+ const APP_KEY_SHAPES: Array<{ match: RegExp; value: () => string; source: string }> = [
211
+ // AES-256-GCM: the key base64-decodes to 32 bytes ("ENCRYPTION_KEY must be 32 bytes (base64-encoded)"); the 32 bytes
212
+ // spell out that they are fake
213
+ { match: /^ENCRYPTION_KEY$/, value: () => Buffer.from('twin-fake-encryption-key-32bytes').toString('base64'), source: "dubinc/dub apps/web/lib/encryption.ts" },
214
+ ];
215
+
216
+ export function fakeEnvValue(name: string): string {
217
+ if (isGoogleOAuthClientEnvName(name)) return fakeGoogleOAuthClientJson();
218
+ for (const shape of APP_KEY_SHAPES) if (shape.match.test(name)) return shape.value();
219
+ if (isGoogleServiceAccountEnvName(name)) return fakeGoogleServiceAccountJson();
220
+ for (const shape of ENDPOINT_SHAPES) if (shape.match.test(name)) return shape.value;
221
+ for (const shape of CREDENTIAL_SHAPES) if (shape.match.test(name)) return shape.value(name);
222
+ const slug = name.toLowerCase().replace(/_/g, '-');
223
+ // A URL-shaped name must be faked as a URL (F4). An SDK handed `twin-fake-x` where it expects an
224
+ // endpoint throws while CONSTRUCTING its client — before it ever issues the request the world
225
+ // could have intercepted, so the failure looks like a twin bug and no ledger entry exists to
226
+ // contradict that. `.invalid` is reserved (RFC 2606) and never resolves, so an untwinned vendor
227
+ // still fails — but as an unreachable HOST, which the diagnostics can name.
228
+ if (/(^|_)(URL|URI|ENDPOINT|HOST)$/.test(name)) return `https://${slug}.invalid`;
229
+ return `twin-fake-${slug}`;
230
+ }
@@ -0,0 +1,90 @@
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, type ColocatedTwinSpec, type HostIsolation } from './host.ts';
16
+
17
+ const argv = process.argv.slice(2);
18
+ const opt = (name: string, fb = ''): string => { const i = argv.indexOf(name); return i >= 0 && argv[i + 1] ? argv[i + 1]! : fb; };
19
+
20
+ const isolation = (opt('--isolation', 'shared') as HostIsolation);
21
+ if (isolation !== 'shared' && isolation !== 'worker') {
22
+ process.stderr.write(`invalid --isolation "${isolation}" (want shared|worker)\n`);
23
+ process.exit(2);
24
+ }
25
+
26
+ const specs: ColocatedTwinSpec[] = [];
27
+ for (let i = 0; i < argv.length; i++) {
28
+ if (argv[i] !== '--spec') continue;
29
+ const raw = argv[i + 1] ?? '';
30
+ const [id, module, exportName, port, root, ...flags] = raw.split('|');
31
+ const ro = flags.find((f) => f === 'ro');
32
+ const scenarioPath = flags.find((f) => f.startsWith('scenario='))?.slice('scenario='.length);
33
+ if (!id || !module || !exportName || !port || !root) {
34
+ process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>])\n`);
35
+ process.exit(2);
36
+ }
37
+ specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}) });
38
+ }
39
+ if (specs.length === 0) { process.stderr.write('no --spec given\n'); 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
+
58
+ function recordGaveUp(twin: string, exits: number, detail: string): void {
59
+ if (!gaveUpSidecar) return;
60
+ let all: Record<string, { at: string; exits: number; detail: string }> = {};
61
+ try {
62
+ if (existsSync(gaveUpSidecar)) all = JSON.parse(readFileSync(gaveUpSidecar, 'utf8'));
63
+ } catch {
64
+ // A torn/partial read (e.g. we crashed mid-write previously) — start fresh rather than
65
+ // let a corrupt sidecar wedge future give-ups from ever being recorded.
66
+ }
67
+ all[twin] = { at: new Date().toISOString(), exits, detail };
68
+ // Atomic publish (temp→rename in the SAME dir), the same discipline the kernel/instance.json
69
+ // writers use — a reader never observes a partially-written sidecar.
70
+ mkdirSync(dirname(gaveUpSidecar), { recursive: true });
71
+ const tmp = `${gaveUpSidecar}.${process.pid}.tmp`;
72
+ writeFileSync(tmp, `${JSON.stringify(all, null, 2)}\n`);
73
+ renameSync(tmp, gaveUpSidecar);
74
+ }
75
+
76
+ // deploy: auto is the kernel's head (core head.ts): a twin performs its own `auto` writes as they happen
77
+ const host = await startColocatedHost(specs, {
78
+ isolation,
79
+ onEvent: (e) => {
80
+ process.stdout.write(`event twin=${e.twin} kind=${e.kind}${e.detail ? ` detail=${e.detail}` : ''}\n`);
81
+ if (e.kind === 'gaveup') recordGaveUp(e.twin, e.exits ?? 0, e.detail ?? '');
82
+ },
83
+ });
84
+ for (const [id, port] of Object.entries(host.ports)) process.stdout.write(`ready id=${id} port=${port}\n`);
85
+ process.stdout.write(`host ready (${specs.length} twins, isolation=${isolation})\n`);
86
+
87
+ const shutdown = async () => { await host.stop(); process.exit(0); };
88
+ process.on('SIGTERM', shutdown);
89
+ process.on('SIGINT', shutdown);
90
+ await keepProcessAlive();
@@ -0,0 +1,23 @@
1
+ // Worker entry for 'worker' isolation: one Worker thread per twin. It owns its event
2
+ // loop + heap, so a CPU hog / leak / hard crash here is contained to this thread — the
3
+ // host does NOT restart it: it gives up on the worker (fires a `gaveup` event → doctor
4
+ // red) without disturbing sibling twins. Posts {type:'ready'} once listening.
5
+ import { parentPort, workerData } from 'node:worker_threads';
6
+ import type { ColocatedTwinSpec } from './host.ts';
7
+
8
+ const spec = workerData as ColocatedTwinSpec;
9
+
10
+ type TwinServer = { port: number; stop: () => void | Promise<void> };
11
+ type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean }) => TwinServer | Promise<TwinServer>;
12
+
13
+ const mod = (await import(spec.module)) as Record<string, unknown>;
14
+ const factory = mod[spec.export] as TwinServerFactory | undefined;
15
+ if (typeof factory !== 'function') {
16
+ throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
17
+ }
18
+
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 });
21
+ parentPort?.postMessage({ type: 'ready', port: server.port });
22
+
23
+ // The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
package/src/host.ts ADDED
@@ -0,0 +1,169 @@
1
+ // Co-located twin host — run many twins in ONE process instead of one OS process
2
+ // per twin. The twin core is a pure, transport-agnostic handler (see a pack's
3
+ // `handle<Vendor>TwinRequest`), so nothing requires a process per twin; this host
4
+ // mounts N of them behind their own ports/URLs (the world env + instance.json are
5
+ // byte-identical to the spawned path — only the process model changes).
6
+ //
7
+ // Isolation is a DIAL, not a binary (the runtime stays minimal — this is still just
8
+ // "bring services up / tear them down", one child process):
9
+ // - 'shared' : all twins share one event loop + heap. Lightest. A thrown request
10
+ // handler is isolated (Bun.serve returns 500; siblings serve on), but a
11
+ // CPU hog / OOM / process.exit in one twin hits all. Default for CI/dev.
12
+ // - 'worker' : one Worker thread per twin → own event loop + own heap, independently
13
+ // isolated. A sibling can spin, leak, or hard-exit without taking the
14
+ // host down — but a crashed twin stays down (doctor red), it is never
15
+ // respawned. Still ONE OS process to the orchestrator.
16
+ // ('process' isolation — one OS process per twin — is the world runtime's existing
17
+ // spawn path; it remains the oracle and the choice for share/sealed/hosted worlds.)
18
+ //
19
+ // ARCHITECTURE: the runtime never imports vendor packs. A twin is named by a module
20
+ // SPECIFIER + export resolved with dynamic import() at boot, so this host (and the
21
+ // world config that drives it) stays vendor-agnostic — exactly like `bin` spawning.
22
+ import { Worker } from 'node:worker_threads';
23
+ import { siblingScript } from './sibling.ts';
24
+ import { pathToFileURL } from 'node:url';
25
+
26
+ /** One twin to mount in the host. `module`/`export` name a `({port,root,readOnly}) => {port,stop}`
27
+ * factory (every pack ships one, e.g. `createStripeTwinServer`). `module` is anything import()
28
+ * resolves: a package name (`@volter/twin-stripe`) or an absolute file path (tests/fixtures). */
29
+ export type ColocatedTwinSpec = {
30
+ id: string;
31
+ module: string;
32
+ export: string;
33
+ port: number;
34
+ root: string;
35
+ readOnly?: boolean;
36
+ /** The world's handlers file for a scenario-carrying (generative) twin — the colocate contract's
37
+ * scenarioPath (R2a): the same file the spawn path passes as --scenario. */
38
+ scenarioPath?: string;
39
+ };
40
+
41
+ export type HostIsolation = 'shared' | 'worker';
42
+
43
+ export type ColocatedHost = {
44
+ isolation: HostIsolation;
45
+ /** Resolved listening port per twin id (equals the requested port). */
46
+ ports: Record<string, number>;
47
+ stop: () => Promise<void>;
48
+ };
49
+
50
+ // `stop` returns void OR a promise (76 of 78 packs return Bun's async server.stop) and the
51
+ // factory itself may be async (fly's createFlyTwinServer) — both shapes are awaited, so
52
+ // "resolves once every twin is listening" is true for async factories and shutdown actually
53
+ // WAITS instead of fire-and-forgetting (§9 skeptic finding M1, 2026-08-31: an async factory
54
+ // made host.stop() throw `s.stop is not a function`, turning every SIGTERM into downWorld's
55
+ // SIGKILL escalation for the whole world).
56
+ type TwinServer = { port: number; stop: () => void | Promise<void> };
57
+ type TwinServerFactory = (opts: { port?: number; root?: string; readOnly?: boolean; scenarioPath?: string }) => TwinServer | Promise<TwinServer>;
58
+
59
+ async function loadFactory(spec: ColocatedTwinSpec): Promise<TwinServerFactory> {
60
+ const mod = (await import(spec.module)) as Record<string, unknown>;
61
+ const factory = mod[spec.export];
62
+ if (typeof factory !== 'function') {
63
+ throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
64
+ }
65
+ return factory as TwinServerFactory;
66
+ }
67
+
68
+ const workerEntry = (): URL => pathToFileURL(siblingScript(import.meta.url, 'host-worker'));
69
+
70
+ /** Per-twin stop budget inside host.stop(). Must stay comfortably under downWorld's 5s
71
+ * SIGTERM→SIGKILL grace (DOWN_GRACE_MS_DEFAULT): the host must reach process.exit(0) even
72
+ * when a twin's stop hangs, or the whole co-located world dies by SIGKILL. */
73
+ const STOP_TIMEOUT_MS = 3000;
74
+
75
+ /**
76
+ * Start a co-located host. Resolves once every twin is listening. The returned `stop()`
77
+ * tears the whole host down (all servers / all workers).
78
+ *
79
+ * `onEvent` is an optional observability hook (logged by the CLI) — it surfaces a twin
80
+ * worker crashing (and being given up on), and stray rejections swallowed in shared mode,
81
+ * so a degraded twin is never a silent fake-success.
82
+ */
83
+ export async function startColocatedHost(
84
+ specs: ColocatedTwinSpec[],
85
+ options: { isolation?: HostIsolation; onEvent?: (e: { twin: string; kind: string; detail?: string; exits?: number }) => void } = {},
86
+ ): Promise<ColocatedHost> {
87
+ const isolation = options.isolation ?? 'shared';
88
+ const onEvent = options.onEvent ?? (() => {});
89
+ const ports: Record<string, number> = {};
90
+ for (const spec of specs) ports[spec.id] = spec.port;
91
+
92
+ if (isolation === 'shared') {
93
+ // Keep one twin's stray async rejection / uncaught throw from killing every other twin in
94
+ // the shared process. (A thrown *request* handler is already contained by Bun.serve → 500.)
95
+ // We log and attribute rather than exit — state stays consistent because every kernel write
96
+ // is atomic (temp→rename) and root-keyed, so a swallowed error can't corrupt a sibling.
97
+ const onRejection = (reason: unknown) => onEvent({ twin: '(host)', kind: 'unhandledRejection', detail: String(reason) });
98
+ const onUncaught = (err: unknown) => onEvent({ twin: '(host)', kind: 'uncaughtException', detail: String(err) });
99
+ process.on('unhandledRejection', onRejection);
100
+ process.on('uncaughtException', onUncaught);
101
+
102
+ const servers: TwinServer[] = [];
103
+ for (const spec of specs) {
104
+ const factory = await loadFactory(spec);
105
+ servers.push(await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}) }));
106
+ }
107
+ return {
108
+ isolation,
109
+ ports,
110
+ stop: async () => {
111
+ process.off('unhandledRejection', onRejection);
112
+ process.off('uncaughtException', onUncaught);
113
+ // EVERY stop is invoked, IN PARALLEL, each under its own timeout, and no outcome —
114
+ // rejection, hang, or slowness — can prevent the others or wedge host.stop() itself.
115
+ // The awaited-sequential first cut re-created the SIGKILL escalation it was fixing
116
+ // through a new door (§9 round two H2, 2026-08-31: one pending/throwing stop — fly's
117
+ // cleanup throws on a failed container removal — left siblings unstopped and the host
118
+ // ignoring SIGTERM forever). The bound stays under downWorld's 5s SIGKILL grace.
119
+ await Promise.allSettled(servers.map((s) =>
120
+ Promise.race([
121
+ Promise.resolve().then(() => s.stop()),
122
+ new Promise((resolveTimeout) => setTimeout(resolveTimeout, STOP_TIMEOUT_MS).unref?.()),
123
+ ]),
124
+ ));
125
+ },
126
+ };
127
+ }
128
+
129
+ // worker isolation — one Worker thread per twin, given up on crash — never respawned.
130
+ const stopping = { value: false };
131
+ const workers = new Map<string, Worker>();
132
+
133
+ const spawnWorker = (spec: ColocatedTwinSpec): Promise<void> =>
134
+ new Promise<void>((resolveReady, rejectReady) => {
135
+ const worker = new Worker(workerEntry(), { workerData: spec });
136
+ // Pure observability, symmetric with `gaveup` below — NOT a supervision hook. Fired exactly
137
+ // once per spawnWorker() call (there's no counter/timer/keep-alive here), so it doubles as a
138
+ // direct tamper guard: a stealth respawn re-adding `spawnWorker(spec)` in the exit handler
139
+ // below would emit a 2nd `spawn` for the same twin, which the isolation test asserts against.
140
+ onEvent({ twin: spec.id, kind: 'spawn', detail: 'worker started' });
141
+ let ready = false;
142
+ worker.on('message', (msg: { type?: string }) => {
143
+ if (msg?.type === 'ready') { ready = true; resolveReady(); }
144
+ });
145
+ worker.on('error', (err) => {
146
+ onEvent({ twin: spec.id, kind: 'error', detail: String(err) });
147
+ if (!ready) rejectReady(err);
148
+ });
149
+ worker.on('exit', (code) => {
150
+ workers.delete(spec.id);
151
+ if (stopping.value || code === 0) return;
152
+ // Minimal-primitive doctrine: no keep-alive supervisor. A crashed worker is NOT
153
+ // respawned — record the give-up (host-cli persists it → doctor red) and stay dead.
154
+ onEvent({ twin: spec.id, kind: 'gaveup', detail: `exit code ${code} — not restarted`, exits: 1 });
155
+ });
156
+ workers.set(spec.id, worker);
157
+ });
158
+
159
+ for (const spec of specs) await spawnWorker(spec);
160
+
161
+ return {
162
+ isolation,
163
+ ports,
164
+ stop: async () => {
165
+ stopping.value = true;
166
+ await Promise.all([...workers.values()].map((w) => w.terminate()));
167
+ },
168
+ };
169
+ }