@volter/world-runtime 2.0.54 → 3.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/dist/known-external-services.json +5 -4
- package/dist/src/actions-runner-cli.d.ts +2 -0
- package/dist/src/actions-runner-cli.js +25 -0
- package/dist/src/actions-runner.d.ts +18 -0
- package/dist/src/actions-runner.js +281 -0
- package/dist/src/ancestry.js +1 -1
- package/dist/src/app-url.js +1 -1
- package/dist/src/backings.d.ts +29 -0
- package/dist/src/backings.js +45 -0
- package/dist/src/branch.js +3 -2
- package/dist/src/browser-proxy-cli.js +1 -1
- package/dist/src/catalog-index.d.ts +38 -0
- package/dist/src/catalog-index.js +37 -0
- package/dist/src/catalog.d.ts +16 -3
- package/dist/src/catalog.js +69 -26
- package/dist/src/changeset.d.ts +1 -1
- package/dist/src/changeset.js +9 -11
- package/dist/src/cli-args.d.ts +4 -0
- package/dist/src/cli-args.js +18 -0
- package/dist/src/cli-commands.d.ts +1 -0
- package/dist/src/cli-commands.js +1216 -0
- package/dist/src/cli.js +8 -1300
- package/dist/src/configs.js +1 -1
- package/dist/src/console-apart.d.ts +2 -2
- package/dist/src/console-apart.js +5 -5
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +1 -1
- package/dist/src/covers.js +94 -30
- package/dist/src/env-output.d.ts +1 -1
- package/dist/src/env-output.js +2 -2
- package/dist/src/fixture-env.js +18 -26
- package/dist/src/host-cli.js +4 -2
- package/dist/src/host-fault-fixture.js +6 -0
- package/dist/src/host-ready.d.ts +8 -2
- package/dist/src/host-ready.js +16 -4
- package/dist/src/host-worker.js +6 -1
- package/dist/src/host.d.ts +2 -0
- package/dist/src/host.js +9 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/init.d.ts +33 -23
- package/dist/src/init.js +228 -96
- package/dist/src/lifecycle-record.js +2 -2
- package/dist/src/local-branches.js +1 -1
- package/dist/src/origin.d.ts +1 -1
- package/dist/src/origin.js +12 -10
- package/dist/src/pack-facts.d.ts +30 -6
- package/dist/src/pack-facts.js +13 -3
- package/dist/src/pglite-backing.js +2 -1
- package/dist/src/placeholder.d.ts +1 -1
- package/dist/src/placeholder.js +12 -4
- package/dist/src/project-inspect.d.ts +1 -1
- package/dist/src/project-inspect.js +2 -2
- package/dist/src/redirect-proxy.d.ts +5 -0
- package/dist/src/redirect-proxy.js +8 -1
- package/dist/src/redis-backing.js +4 -2
- package/dist/src/reflect.js +1 -1
- package/dist/src/root.d.ts +11 -33
- package/dist/src/root.js +28 -59
- package/dist/src/runtime-test-support.js +1 -1
- package/dist/src/runtime.d.ts +7 -1
- package/dist/src/runtime.js +317 -53
- package/dist/src/schema.d.ts +27 -5
- package/dist/src/schema.js +13 -12
- package/dist/src/served-world.d.ts +11 -35
- package/dist/src/served-world.js +54 -141
- package/dist/src/tail.js +1 -1
- package/dist/src/up-cli.d.ts +3 -0
- package/dist/src/up-cli.js +55 -0
- package/dist/src/websocket-relay.d.ts +1 -1
- package/dist/src/world-origins.d.ts +1 -1
- package/dist/src/world-origins.js +1 -1
- package/dist/src/world-view.d.ts +1 -1
- package/dist/src/world-view.js +2 -2
- package/known-external-services.json +5 -4
- package/package.json +11 -11
- package/src/actions-runner-cli.ts +24 -0
- package/src/actions-runner.ts +244 -0
- package/src/ancestry.ts +1 -1
- package/src/app-url.ts +1 -1
- package/src/backings.ts +55 -0
- package/src/branch.ts +3 -2
- package/src/browser-proxy-cli.ts +1 -1
- package/src/catalog-index.ts +44 -0
- package/src/catalog.ts +67 -24
- package/src/changeset.ts +10 -13
- package/src/cli-args.ts +17 -0
- package/src/cli-commands.ts +1070 -0
- package/src/cli.ts +9 -1139
- package/src/configs.ts +1 -1
- package/src/console-apart.ts +5 -5
- package/src/consumers.ts +1 -1
- package/src/covers.ts +83 -30
- package/src/env-output.ts +2 -2
- package/src/fixture-env.ts +18 -26
- package/src/host-cli.ts +4 -2
- package/src/host-ready.ts +13 -4
- package/src/host-worker.ts +6 -2
- package/src/host.ts +11 -2
- package/src/index.ts +2 -2
- package/src/init.ts +222 -101
- package/src/lifecycle-record.ts +2 -2
- package/src/local-branches.ts +1 -1
- package/src/origin.ts +12 -10
- package/src/pack-facts.ts +22 -6
- package/src/pglite-backing.ts +2 -1
- package/src/placeholder.ts +13 -4
- package/src/project-inspect.ts +2 -2
- package/src/redirect-proxy.ts +9 -1
- package/src/redis-backing.ts +4 -2
- package/src/reflect.ts +1 -1
- package/src/root.ts +33 -65
- package/src/runtime-test-support.ts +1 -1
- package/src/runtime.ts +261 -55
- package/src/schema.ts +26 -13
- package/src/served-world.ts +50 -130
- package/src/tail.ts +1 -1
- package/src/up-cli.ts +55 -0
- package/src/websocket-relay.ts +1 -1
- package/src/world-origins.ts +1 -1
- package/src/world-view.ts +3 -3
package/dist/src/configs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { copyFileSync, constants, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
-
import { getActiveWorldStore, stateDirName } from '@volter/world-core';
|
|
3
|
+
import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime';
|
|
4
4
|
import { assertWorldConfig, LEGACY_WORLD_SELECTION, worldConfigDocument } from "./schema.js";
|
|
5
5
|
export function resolveConfigPath(config, root = process.cwd()) {
|
|
6
6
|
const candidates = config.includes('/') || config.endsWith('.json')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type HttpServer } from '@volter/world-core';
|
|
1
|
+
import { type HttpServer } from '@volter/world-core/runtime';
|
|
2
2
|
/** What a console offers a host: its shell and assets under `base`, told where the Worlds are served. */
|
|
3
3
|
export type ConsoleMount = {
|
|
4
4
|
handle: (request: Request, base: string, opts?: {
|
|
@@ -23,7 +23,7 @@ export declare function serveConsoleApart(mount: ConsoleMount, opts: {
|
|
|
23
23
|
}>;
|
|
24
24
|
/** The console for a World served elsewhere (a hosted World), on this machine. Two local listeners, as
|
|
25
25
|
* `volter world serve` has: the console on one origin, its doors forwarded to the host; and the Worlds'
|
|
26
|
-
* origin on the next port, forwarding everything (a
|
|
26
|
+
* origin on the next port, forwarding everything (a vendor's screens, the session door) to the host, so
|
|
27
27
|
* the console and the pages twins serve are same-site as they are locally, and still two origins. A
|
|
28
28
|
* host keeps its tenants' names to itself, so the World is named by the URL: `<origin>/<org>/<world>`. */
|
|
29
29
|
export declare function serveConsoleFor(target: string, opts?: {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// THE CONSOLE ON AN ORIGIN OF ITS OWN. The console keeps the token a person types in its tab's
|
|
2
|
-
// storage; the Worlds' origin serves what twins serve (a
|
|
2
|
+
// storage; the Worlds' origin serves what twins serve (a vendor's screens, a tunnel's visitor pages,
|
|
3
3
|
// an app's own HTML), and a script there could read any storage of that origin. So a host serves the
|
|
4
4
|
// console on a listener of its own: the console's shell and bundle, and the host's `/-/` doors
|
|
5
5
|
// forwarded to the host — never a twin's path, so no twin-served script ever runs on this origin.
|
|
6
|
-
// The console opens a World's pages (a
|
|
7
|
-
import { serveHttp } from '@volter/world-core';
|
|
6
|
+
// The console opens a World's pages (a vendor's screens) on the Worlds' origin, which it is told.
|
|
7
|
+
import { serveHttp } from '@volter/world-core/runtime';
|
|
8
8
|
export const CONSOLE_BASE = '/-/console';
|
|
9
9
|
/** Serve `mount` on its own listener; doors under `/-/` go to `upstream` (the Worlds' origin). */
|
|
10
10
|
/** `upstream`: where this listener reaches the host's doors (its own address); `worlds`: the origin the
|
|
@@ -41,7 +41,7 @@ export async function serveConsoleApart(mount, opts) {
|
|
|
41
41
|
}
|
|
42
42
|
/** The console for a World served elsewhere (a hosted World), on this machine. Two local listeners, as
|
|
43
43
|
* `volter world serve` has: the console on one origin, its doors forwarded to the host; and the Worlds'
|
|
44
|
-
* origin on the next port, forwarding everything (a
|
|
44
|
+
* origin on the next port, forwarding everything (a vendor's screens, the session door) to the host, so
|
|
45
45
|
* the console and the pages twins serve are same-site as they are locally, and still two origins. A
|
|
46
46
|
* host keeps its tenants' names to itself, so the World is named by the URL: `<origin>/<org>/<world>`. */
|
|
47
47
|
export async function serveConsoleFor(target, opts = {}) {
|
|
@@ -86,7 +86,7 @@ export async function serveConsoleFor(target, opts = {}) {
|
|
|
86
86
|
// the World session's cookie, kept on this plain-http loopback origin (a Secure cookie is https-only)
|
|
87
87
|
for (const c of answer.headers.getSetCookie())
|
|
88
88
|
out.append('set-cookie', c.replace(/;\s*Secure/i, ''));
|
|
89
|
-
// the host's own redirects (the session door's
|
|
89
|
+
// the host's own redirects (the session door's) stay on this origin
|
|
90
90
|
const location = out.get('location');
|
|
91
91
|
if (location?.startsWith(origin))
|
|
92
92
|
out.set('location', `${url.origin}${location.slice(origin.length)}`);
|
package/dist/src/consumers.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { hostname } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { stateDirName, withFileLock } from '@volter/world-core';
|
|
5
|
+
import { stateDirName, withFileLock } from '@volter/world-core/runtime';
|
|
6
6
|
export const CONSUMER_HEARTBEAT_MS = 5000;
|
|
7
7
|
const STALE_MS = 30_000;
|
|
8
8
|
export function checkedOwner(owner) {
|
package/dist/src/covers.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export declare function isMappedScopeSibling(name: string): boolean;
|
|
|
77
77
|
* `'acknowledged'` was adjudicated acceptable and auto-acknowledges with its recorded
|
|
78
78
|
* reason); `notExternal` suppresses vendor-adjacent glue (protocol/type/UI helpers, auth
|
|
79
79
|
* libraries); `patterns` are the external-shape heuristics. AN ENTRY DIES THE DAY ITS PACK
|
|
80
|
-
* IS BORN — scripts/packless-claims.
|
|
80
|
+
* IS BORN — scripts/packless-claims.ts enforces disjointness with pack adoption and
|
|
81
81
|
* SDK_TWINS (the `@planetscale/database`/twin#255 lesson, mechanized). Lazy + cached like
|
|
82
82
|
* pack-facts.ts's loader — though note this module ALREADY does module-scope I/O via the
|
|
83
83
|
* overlayCoversMaps call below, so laziness here is consistency, not a load-bearing
|
package/dist/src/covers.js
CHANGED
|
@@ -21,15 +21,16 @@
|
|
|
21
21
|
// coverage. An external-service-shaped dep/env var with no vendor mapping →
|
|
22
22
|
// unknown-sdk → exit 1 by default (`--allow-unknown` downgrades to a warning:
|
|
23
23
|
// unknowns still print, but stop failing the proof).
|
|
24
|
-
import { readFileSync } from 'node:fs';
|
|
25
|
-
import { dirname, join, resolve } from 'node:path';
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
25
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { vendorOfPackModule } from '@volter/world-core/runtime';
|
|
27
28
|
import { loadWorldConfig } from "./configs.js";
|
|
28
29
|
import { injectableVendorKeys, loadInject, twinUrlVendorFor } from "./inject-map.js";
|
|
29
|
-
import { connectionProtocol, projectConnections, projectDependencies, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from "./project-inspect.js";
|
|
30
|
+
import { connectionProtocol, projectConnections, projectDependencies, projectManifestDirs, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from "./project-inspect.js";
|
|
30
31
|
import { overlayCoversMaps, packFacts } from "./pack-facts.js";
|
|
31
32
|
import { statusWorld } from "./runtime.js";
|
|
32
|
-
import { worldSelectionIncludes } from "./schema.js";
|
|
33
|
+
import { ISSUE_ENV_PREFIX, worldSelectionIncludes } from "./schema.js";
|
|
33
34
|
/** identifiers are compared case/punctuation-insensitively: 's3', 'S3_TWIN_URL' stem,
|
|
34
35
|
* a 'stripe' service id and the 'ai-gateway' pack name all normalize cleanly. */
|
|
35
36
|
function normalizeId(text) {
|
|
@@ -71,7 +72,7 @@ export function injectorEnvNameForKey(key) {
|
|
|
71
72
|
/** Vendor-shaped env var detection (the secondary signal). The var must end in a
|
|
72
73
|
* credential/endpoint suffix; the remaining stem (framework prefixes stripped,
|
|
73
74
|
* normalized) is looked up here. */
|
|
74
|
-
// Exported for scripts/packless-claims.
|
|
75
|
+
// Exported for scripts/packless-claims.ts, which checks the packless registry against
|
|
75
76
|
// the OVERLAID map (legacy entries + descriptor adoption.envStems in one view) — importing
|
|
76
77
|
// the real map is unbreakable by formatting, unlike the source-parsing it replaced (§9
|
|
77
78
|
// skeptic M3, 2026-08-31: a reflow or quote-style change silently emptied the parsed set).
|
|
@@ -114,6 +115,24 @@ function envVendorStem(name, suffix) {
|
|
|
114
115
|
return null;
|
|
115
116
|
return normalizeId(stripped.slice(0, match.index));
|
|
116
117
|
}
|
|
118
|
+
/** The vendor a credential-shaped env name's stem names: the stem left once the suffix is stripped, or else the longest
|
|
119
|
+
* declared stem the name begins with (googleoauth's GOOGLE_CLIENT for GOOGLE_CLIENT_ID, whose `_CLIENT_ID` suffix
|
|
120
|
+
* leaves only GOOGLE). */
|
|
121
|
+
function envStemVendor(name, suffix) {
|
|
122
|
+
const stem = envVendorStem(name, suffix);
|
|
123
|
+
if (stem === null)
|
|
124
|
+
return undefined;
|
|
125
|
+
if (ENV_STEM_VENDORS[stem])
|
|
126
|
+
return ENV_STEM_VENDORS[stem];
|
|
127
|
+
const words = name.replace(ENV_FRAMEWORK_PREFIX, '').split('_');
|
|
128
|
+
// a declared stem of more than one word: a one-word stem (X's `X`) would claim every name it begins
|
|
129
|
+
for (let k = words.length - 1; k >= 2; k--) {
|
|
130
|
+
const vendor = ENV_STEM_VENDORS[normalizeId(words.slice(0, k).join('_'))];
|
|
131
|
+
if (vendor)
|
|
132
|
+
return vendor;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
117
136
|
/** Is this env NAME shaped like a CREDENTIAL/endpoint at all (any broad vendor suffix)? The
|
|
118
137
|
* question `init` asks before deciding whether a value in a repo's `.env.example` may be copied
|
|
119
138
|
* verbatim: `APP_NAME` may, `ANYTHING_SECRET` may not — a committed example file is exactly where
|
|
@@ -121,7 +140,7 @@ function envVendorStem(name, suffix) {
|
|
|
121
140
|
* ALWAYS replaced by a fake, whether or not its stem maps to a vendor this repo twins. Exported
|
|
122
141
|
* from here so the emission path and the proof read ONE definition of "credential-shaped". */
|
|
123
142
|
export function isCredentialShapedEnvName(name) {
|
|
124
|
-
return ENV_SUFFIX_BROAD.test(name.replace(ENV_FRAMEWORK_PREFIX, ''));
|
|
143
|
+
return name === 'GOOGLE_APPLICATION_CREDENTIALS' || ENV_SUFFIX_BROAD.test(name.replace(ENV_FRAMEWORK_PREFIX, ''));
|
|
125
144
|
}
|
|
126
145
|
/** The vendor a credential-shaped env NAME betrays, or null. Same broad-suffix route the
|
|
127
146
|
* coverage proof's secondary signal uses (`STRIPE_SECRET_KEY` → 'stripe'). */
|
|
@@ -138,8 +157,7 @@ export function envNameVendor(name) {
|
|
|
138
157
|
// So: this asks "is this name a VENDOR CREDENTIAL that must be faked?" (a hostname is not), while
|
|
139
158
|
// `detectRepoVendors` asks "which vendors does this repo talk to?" (the mail path certainly is
|
|
140
159
|
// one). Same string, two honest answers.
|
|
141
|
-
|
|
142
|
-
return stem === null ? null : ENV_STEM_VENDORS[stem] ?? null;
|
|
160
|
+
return envStemVendor(name, ENV_SUFFIX_BROAD) ?? null;
|
|
143
161
|
}
|
|
144
162
|
/** SMTP-shaped env names — the raw-protocol mail path (nodemailer et al.) the HTTP-vendor
|
|
145
163
|
* heuristics above are structurally blind to. The Cal.com blind run's booking-confirmation
|
|
@@ -168,7 +186,7 @@ function isSmtpEnvSignal(name) {
|
|
|
168
186
|
const SDK_SCOPE_VENDORS = {
|
|
169
187
|
// (empty — every entry has moved onto its pack descriptor. See the note below.)
|
|
170
188
|
};
|
|
171
|
-
// descriptor-first migration (
|
|
189
|
+
// descriptor-first migration (architecture.md, "The descriptor"), COMPLETE for these three tables: every pack declares its own
|
|
172
190
|
// scopes / env stems / world ids (`adoption` on its descriptor, compiled into the committed
|
|
173
191
|
// pack-facts artifact), and all three literals above are now empty — they are populated entirely
|
|
174
192
|
// by the overlay below. They are kept as the overlay's targets, not as homes: the object identity
|
|
@@ -261,10 +279,11 @@ function servicePackNames(service) {
|
|
|
261
279
|
...(service.colocate?.module ? [service.colocate.module] : []),
|
|
262
280
|
];
|
|
263
281
|
return commandParts.flatMap((part) => {
|
|
264
|
-
const fromPath = part.match(/
|
|
282
|
+
const fromPath = part.match(/(?:^|[/\\])twin-packs-p[23][/\\]([a-z0-9-]+)[/\\]/);
|
|
265
283
|
const fromBin = part.match(/^world-([a-z0-9-]+)$/);
|
|
266
284
|
const fromPkg = part.match(/(?:^|[/\\])@volter\/twin-([a-z0-9-]+)(?:[/\\]|$)/);
|
|
267
|
-
|
|
285
|
+
const fromManifest = /\.(?:[cm]?js|tsx?)$/.test(part) ? vendorOfPackModule(part) : undefined;
|
|
286
|
+
return [fromManifest, fromPath?.[1], fromBin?.[1], fromPkg?.[1]].filter((name) => name !== undefined);
|
|
268
287
|
});
|
|
269
288
|
}
|
|
270
289
|
/** Endpoint wiring owned by a service, never a random URL from global config/env. Config
|
|
@@ -282,15 +301,10 @@ function serviceConnections(services, effectiveEnv) {
|
|
|
282
301
|
const values = instance ? service.env : config?.injectEnvTemplates ?? {};
|
|
283
302
|
for (const [name, value] of Object.entries(values)) {
|
|
284
303
|
const protocol = connectionProtocol(value);
|
|
285
|
-
const command = [...(Array.isArray(service.command) ? service.command : []), ...('args' in service ? service.args ?? [] : [])];
|
|
286
304
|
// The pack-transport veto is about NATIVE schemes (a mysql:// template on an HTTP twin); an
|
|
287
305
|
// http(s) endpoint template on any twin is that twin's own HTTP door.
|
|
288
|
-
const incompatiblePack = protocol !== null && facts.some((fact) => {
|
|
289
|
-
|
|
290
|
-
return false;
|
|
291
|
-
return fact.transport !== 'raw-tcp' || !Object.values(fact.endpointEnv?.templates ?? {})
|
|
292
|
-
.some((template) => connectionProtocol(template) === protocol && protocol !== null);
|
|
293
|
-
});
|
|
306
|
+
const incompatiblePack = protocol !== null && facts.some((fact) => fact.transport !== 'raw-tcp' || !Object.values(fact.endpointEnv?.templates ?? {})
|
|
307
|
+
.some((template) => connectionProtocol(template) === protocol && protocol !== null));
|
|
294
308
|
let routed = false;
|
|
295
309
|
if (protocol !== null && !incompatiblePack) {
|
|
296
310
|
if (instance) {
|
|
@@ -323,7 +337,7 @@ function serviceConnections(services, effectiveEnv) {
|
|
|
323
337
|
bindings.set(name, { protocol: template.includes('${url}') || /^https?:/.test(template) ? 'http' : null, label: `${service.id} (${name}; cliRedirect)` });
|
|
324
338
|
}
|
|
325
339
|
for (const mapping of config?.external?.discover ?? []) {
|
|
326
|
-
bindings.set(mapping.as, { protocol: null, label: `${service.id} (${mapping.as}; discover after up)` });
|
|
340
|
+
bindings.set(mapping.as, { protocol: mapping.protocol ?? null, label: `${service.id} (${mapping.as}; discover after up)` });
|
|
327
341
|
}
|
|
328
342
|
}
|
|
329
343
|
}
|
|
@@ -362,11 +376,7 @@ function addServiceIds(ids, service) {
|
|
|
362
376
|
.some(([name, value]) => httpNames.has(name) && (/^(?:https?|wss?):/.test(value) || value === '${url}'));
|
|
363
377
|
const nativeOnly = packNames.length === 0 && endpointValues.some((value) => connectionProtocol(value) !== null) &&
|
|
364
378
|
!vendorHttpEndpoint;
|
|
365
|
-
const
|
|
366
|
-
const mode = packFacts()[name]?.nativeTransport;
|
|
367
|
-
return mode && [...(Array.isArray(service.command) ? service.command : []), ...(service.args ?? [])].includes(mode.flag);
|
|
368
|
-
});
|
|
369
|
-
const interceptable = !nativeOnly && !nativeMode && (injectorReads || appConfigured || hostMatched);
|
|
379
|
+
const interceptable = !nativeOnly && (injectorReads || appConfigured || hostMatched);
|
|
370
380
|
// twin + external services cover their vendor by identity; a plain 'process'
|
|
371
381
|
// service only counts via an explicit twin marker (injectEnv/*_TWIN_URL, pack path).
|
|
372
382
|
if (type === 'twin' || type === 'external') {
|
|
@@ -407,7 +417,14 @@ export function worldTwinInventory(world, root) {
|
|
|
407
417
|
const ids = new Map();
|
|
408
418
|
for (const service of config.services)
|
|
409
419
|
addServiceIds(ids, service);
|
|
410
|
-
|
|
420
|
+
const connections = serviceConnections(config.services);
|
|
421
|
+
// a name the World's env leaves to a twin's credential door (`$issue:<service>`) is that twin's, whatever scheme the
|
|
422
|
+
// issued value has: the twin that issues it serves it
|
|
423
|
+
for (const [name, value] of Object.entries(config.env ?? {})) {
|
|
424
|
+
if (value.startsWith(ISSUE_ENV_PREFIX))
|
|
425
|
+
connections.set(name, { protocol: 'issued', label: `${value.slice(ISSUE_ENV_PREFIX.length)} (${name}; issued)` });
|
|
426
|
+
}
|
|
427
|
+
return { source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services) };
|
|
411
428
|
}
|
|
412
429
|
catch (error) {
|
|
413
430
|
const configError = error instanceof Error ? error.message : String(error);
|
|
@@ -495,8 +512,7 @@ export function detectRepoVendors(repoPath) {
|
|
|
495
512
|
detected.set('smtp', [...(detected.get('smtp') ?? []), `env: ${name}`]);
|
|
496
513
|
continue;
|
|
497
514
|
}
|
|
498
|
-
const
|
|
499
|
-
const vendor = stem === null ? undefined : ENV_STEM_VENDORS[stem];
|
|
515
|
+
const vendor = envStemVendor(name, ENV_SUFFIX_BROAD);
|
|
500
516
|
if (vendor)
|
|
501
517
|
detected.set(vendor, [...(detected.get(vendor) ?? []), `env: ${name}`]);
|
|
502
518
|
}
|
|
@@ -545,6 +561,53 @@ export function detectRepoVendors(repoPath) {
|
|
|
545
561
|
detected.set(vendor, [...(detected.get(vendor) ?? []), source]);
|
|
546
562
|
}
|
|
547
563
|
}
|
|
564
|
+
// a platform's config file that betrays the vendor (a vercel.json declaring crons: the platform calls the app's routes)
|
|
565
|
+
for (const [vendor, facts] of Object.entries(packFacts())) {
|
|
566
|
+
for (const config of facts.adoption?.configFiles ?? []) {
|
|
567
|
+
for (const dir of projectManifestDirs(repo)) {
|
|
568
|
+
const path = join(dir, config.file);
|
|
569
|
+
if (!existsSync(path))
|
|
570
|
+
continue;
|
|
571
|
+
let parsed;
|
|
572
|
+
try {
|
|
573
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (config.when !== undefined && !(parsed && typeof parsed === 'object' && config.when in parsed))
|
|
579
|
+
continue;
|
|
580
|
+
const where = relative(repo, path) || config.file;
|
|
581
|
+
const evidence = config.usage === 'application' ? `config: ${where}${config.when ? ` (${config.when})` : ''}` : `${config.usage}-tool: config: ${where}`;
|
|
582
|
+
detected.set(vendor, [...(detected.get(vendor) ?? []), evidence]);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Exact names a descriptor's credential door fills are evidence too (ADC names a file, not a _KEY scalar), when the
|
|
587
|
+
// name names the vendor: its first word is the first word of one of the vendor's env stems (GOOGLE_APPLICATION_CREDENTIALS
|
|
588
|
+
// for GOOGLE_OAUTH), or it holds one of the stems whole (SUPERCODE_TUNNEL_SERVER_URL for TUNNEL). A generic name a door
|
|
589
|
+
// also fills (DATABASE_URL, NODE_AUTH_TOKEN) is every vendor's and so no vendor's evidence; a name that already is the
|
|
590
|
+
// vendor's evidence is not counted twice.
|
|
591
|
+
for (const [vendor, facts] of Object.entries(packFacts())) {
|
|
592
|
+
const stems = facts.adoption?.envStems ?? [];
|
|
593
|
+
const words = new Set(stems.map((stem) => stem.split('_')[0]));
|
|
594
|
+
const names = (name) => {
|
|
595
|
+
const bare = name.replace(ENV_FRAMEWORK_PREFIX, '');
|
|
596
|
+
return words.has(bare.split('_')[0]) || stems.some((stem) => `_${bare}_`.includes(`_${stem}_`));
|
|
597
|
+
};
|
|
598
|
+
for (const name of Object.keys(facts.credentialDoor?.fill ?? {})) {
|
|
599
|
+
if (!envNames.includes(name) || !names(name))
|
|
600
|
+
continue;
|
|
601
|
+
if (!(detected.get(vendor) ?? []).includes(`env: ${name}`))
|
|
602
|
+
detected.set(vendor, [...(detected.get(vendor) ?? []), `env: ${name} (credential issuer)`]);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Expand the declared issuer relation transitively, once per vendor; cycles cannot keep detection running.
|
|
606
|
+
for (const vendor of detected.keys()) {
|
|
607
|
+
const issuer = packFacts()[vendor]?.credentialIssuer;
|
|
608
|
+
if (issuer && !detected.has(issuer))
|
|
609
|
+
detected.set(issuer, [`credential issuer: ${vendor}`]);
|
|
610
|
+
}
|
|
548
611
|
// unknown external-service-shaped signals (no vendor mapping). A dep from a scope with a
|
|
549
612
|
// twin-mapped sibling counts even when its own name is not service-shaped — the Dub run's
|
|
550
613
|
// @upstash/redis + @upstash/ratelimit disappeared exactly here while @upstash/qstash mapped.
|
|
@@ -569,7 +632,7 @@ export function detectRepoVendors(repoPath) {
|
|
|
569
632
|
if (isSmtpEnvSignal(name))
|
|
570
633
|
continue; // now a DETECTED vendor (above), no longer an unknown
|
|
571
634
|
const stem = envVendorStem(name, ENV_SUFFIX_STRICT);
|
|
572
|
-
if (stem === null ||
|
|
635
|
+
if (stem === null || envStemVendor(name, ENV_SUFFIX_STRICT) || ENV_STEM_IGNORE.has(stem))
|
|
573
636
|
continue;
|
|
574
637
|
// The registry's notExternal suppressions apply to ENV STEMS too — the census adjudicated
|
|
575
638
|
// 40 app-internal stems ("names no vendor") that this loop kept flagging because only the
|
|
@@ -709,9 +772,10 @@ export function coverWorld(world, repoPath, options = {}) {
|
|
|
709
772
|
// An HTTP-valued endpoint variable the world injects from a twin (its endpoint env or a
|
|
710
773
|
// template) is routed to that twin; only an HTTP URL nobody in the world supplies stays unknown.
|
|
711
774
|
const injectedHttp = requirement.protocol === null && binding?.protocol === 'http';
|
|
712
|
-
const
|
|
775
|
+
const issued = binding?.protocol === 'issued';
|
|
776
|
+
const unresolved = !injectedHttp && !issued && (requirement.protocol === null || requirement.env === undefined || (binding !== undefined && binding.protocol === null));
|
|
713
777
|
let status = unresolved ? 'unknown-sdk'
|
|
714
|
-
: injectedHttp || binding?.protocol === requirement.protocol ? 'covered' : 'missing';
|
|
778
|
+
: injectedHttp || issued || binding?.protocol === requirement.protocol ? 'covered' : 'missing';
|
|
715
779
|
if (status !== 'covered' && acknowledge[vendor] !== undefined) {
|
|
716
780
|
status = 'acknowledged';
|
|
717
781
|
acknowledged.push({ vendor, reason: acknowledge[vendor] });
|
package/dist/src/env-output.d.ts
CHANGED
|
@@ -9,6 +9,6 @@ export type EnvOutputClaim = {
|
|
|
9
9
|
backupDir: string;
|
|
10
10
|
};
|
|
11
11
|
/** Refuse a custody/input file before any World service or lifecycle mutation starts. */
|
|
12
|
-
export declare function claimEnvOutput(path: string, backupDir: string,
|
|
12
|
+
export declare function claimEnvOutput(path: string, backupDir: string, priors?: PriorOutput[], force?: boolean): EnvOutputClaim;
|
|
13
13
|
export declare function publishEnvOutput(claim: EnvOutputClaim, contents: string): void;
|
|
14
14
|
export {};
|
package/dist/src/env-output.js
CHANGED
|
@@ -15,9 +15,9 @@ function inspect(path) {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
/** Refuse a custody/input file before any World service or lifecycle mutation starts. */
|
|
18
|
-
export function claimEnvOutput(path, backupDir,
|
|
18
|
+
export function claimEnvOutput(path, backupDir, priors = [], force = false) {
|
|
19
19
|
const contents = inspect(path);
|
|
20
|
-
if (contents !== undefined && !force && !(prior
|
|
20
|
+
if (contents !== undefined && !force && !priors.some((prior) => prior.path === path && Buffer.from(prior.contents).equals(contents))) {
|
|
21
21
|
throw new Error(`World refuses existing --env-out file it does not own: ${path}. Choose a new output path; never use a custody file. --force explicitly replaces a file with a private backup.`);
|
|
22
22
|
}
|
|
23
23
|
if (force && contents !== undefined) {
|
package/dist/src/fixture-env.js
CHANGED
|
@@ -134,28 +134,20 @@ export function isGoogleServiceAccountEnvName(name) {
|
|
|
134
134
|
* hostname and sends the request to the local twin, while the slug remains unmistakably fake.
|
|
135
135
|
*
|
|
136
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
|
|
138
|
-
*
|
|
137
|
+
* world-runtime must not import a pack, and the tests pin every value against the injector's host
|
|
138
|
+
* rules (the pack's facts) so either side changing makes the contract fail loudly. */
|
|
139
139
|
const ENDPOINT_SHAPES = [
|
|
140
|
-
// the vendor's own API URL, which the World routes to its twin: an app that reads a base-URL override (empty in its
|
|
141
|
-
// example, the vendor's URL as its default) gets the vendor's URL, never an unreachable fake
|
|
142
|
-
{ match: /^DYNADOT_BASE_URL$/, value: 'https://api.dynadot.com/api3.json', source: 'packages/twin/dynadot/src/index.ts hosts (api.dynadot.com); dubinc/dub apps/web/lib/dynadot/client.ts default' },
|
|
143
140
|
// Vercel KV's REST URL (the Upstash Redis REST API under Vercel's names): an upstash host the twin serves
|
|
144
|
-
{ match: /(^|_)KV_REST_API_URL$/, value: 'https://twin-fake.upstash.io', source: '
|
|
141
|
+
{ match: /(^|_)KV_REST_API_URL$/, value: 'https://twin-fake.upstash.io', source: 'the upstash pack\'s host rules (.upstash.io); lukevella/rallly KV_REST_API_URL' },
|
|
145
142
|
{
|
|
146
143
|
match: /(^|_)UPSTASH_REDIS_REST_URL$/,
|
|
147
144
|
value: 'https://twin-fake.upstash.io',
|
|
148
|
-
source: '
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
match: /(^|_)UPSTASH_VECTOR_REST_URL$/,
|
|
152
|
-
value: 'https://twin-fake-us1-vector.upstash.io',
|
|
153
|
-
source: 'packages/world-core/inject.cjs VENDOR_HOSTS.upstashvector',
|
|
145
|
+
source: 'the upstash pack\'s host rules (.upstash.io)',
|
|
154
146
|
},
|
|
155
147
|
{
|
|
156
148
|
match: /(^|_)TINYBIRD_API_URL$/,
|
|
157
149
|
value: 'https://api.tinybird.co',
|
|
158
|
-
source: '
|
|
150
|
+
source: 'the tinybird pack\'s host rules (api.tinybird.co)',
|
|
159
151
|
},
|
|
160
152
|
];
|
|
161
153
|
/** Credentials whose SHAPE the receiving twin actually checks.
|
|
@@ -174,19 +166,20 @@ const CREDENTIAL_SHAPES = [
|
|
|
174
166
|
// application id, so a structurally wrong fake breaks before any call reaches the twin.
|
|
175
167
|
{ 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' },
|
|
176
168
|
// resend-twin.ts enforces bearer auth of the form `re_<...>` whenever a key is presented.
|
|
177
|
-
{ match: /(^|_)RESEND_[A-Z0-9_]*(KEY|TOKEN|SECRET)$/, value: (name) => `re_twinfake_${name.toLowerCase().replace(/_/g, '')}`, source: '
|
|
169
|
+
{ match: /(^|_)RESEND_[A-Z0-9_]*(KEY|TOKEN|SECRET)$/, value: (name) => `re_twinfake_${name.toLowerCase().replace(/_/g, '')}`, source: 'earlier @volter/twin-resend resend-twin.ts' },
|
|
178
170
|
// The vercel twin answers only a bearer it minted, its bootstrap token first (`vercel.platform.token_identity`); any
|
|
179
171
|
// other value is Vercel's 403 "invalid token", so every call an app makes to its Vercel project fails.
|
|
180
|
-
{ match: /(^|_)VERCEL_[A-Z0-9_]*(TOKEN|API_KEY)$/, value: () => 'vercel_twin_bootstrap_token', source: '
|
|
172
|
+
{ match: /(^|_)VERCEL_[A-Z0-9_]*(TOKEN|API_KEY)$/, value: () => 'vercel_twin_bootstrap_token', source: 'earlier @volter/twin-vercel vercel-runtime.ts VERCEL_TWIN_TOKEN' },
|
|
181
173
|
// The tremendous twin takes a key of the shape Tremendous issues, `TEST_` for the sandbox organization at
|
|
182
174
|
// testflight.tremendous.com and `PROD_` for production (manifest.ts `auth.keyFormat`); any other value is Tremendous's
|
|
183
175
|
// 401. The `TEST_` prefix is what selects the sandbox host in clients that branch on it (dub's
|
|
184
176
|
// lib/tremendous/configuration.ts).
|
|
185
|
-
{ match: /(^|_)TREMENDOUS_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'TEST_twin_sandbox_api_key', source: '
|
|
186
|
-
// Clerk: the SECRET key is
|
|
177
|
+
{ match: /(^|_)TREMENDOUS_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'TEST_twin_sandbox_api_key', source: 'earlier @volter/twin-tremendous manifest.ts auth.keyFormat' },
|
|
178
|
+
// Clerk: the SECRET key is the instance's own (the clerk twin takes this one and the keys its API Keys door makes,
|
|
179
|
+
// earlier @volter/twin-clerk server.ts CLERK_TWIN_SECRET_KEY), and the PUBLISHABLE key is parsed by
|
|
187
180
|
// clerk-js — `pk_test_<base64(frontendApiHost$)>` names the frontend-API host the browser calls.
|
|
188
181
|
// The world issues one naming the dev-instance family the clerk pack CLAIMS by suffix
|
|
189
|
-
// (`.clerk.accounts.dev`,
|
|
182
|
+
// (`.clerk.accounts.dev`, earlier @volter/twin-clerk index.ts hosts), so the boundary — the injector
|
|
190
183
|
// server-side, the browser proxy browser-side — resolves it to the twin, and an app CSP that
|
|
191
184
|
// allowlists that host stays legal (peak drive 2026-08-26).
|
|
192
185
|
// Stripe's keys carry their kind in their prefix, and applications check it before calling (Cal.com's Stripe app
|
|
@@ -194,15 +187,14 @@ const CREDENTIAL_SHAPES = [
|
|
|
194
187
|
{ match: /(^|_)STRIPE_[A-Z0-9_]*WEBHOOK_SECRET$/, value: () => 'whsec_twinfake0000000000000000000000000000', source: 'https://docs.stripe.com/webhooks#verify-events' },
|
|
195
188
|
{ match: /(^|_)STRIPE_[A-Z0-9_]*(SECRET|PRIVATE|API)(_API)?_KEY$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'https://docs.stripe.com/keys (sk_test_)' },
|
|
196
189
|
{ match: /(^|_)STRIPE_[A-Z0-9_]*(PUBLISHABLE|PUBLIC)(_API)?_KEY$/, value: () => 'pk_test_twinfake000000000000000000000000000000', source: 'https://docs.stripe.com/keys (pk_test_)' },
|
|
197
|
-
// The slack twin answers a bearer that is none of Slack's token kinds with `invalid_auth`, as Slack does; any `xoxb-`
|
|
198
|
-
// acts as the workspace bot, and an app-level token is `xapp-`.
|
|
199
|
-
{ match: /(^|_)SLACK_[A-Z0-9_]*APP_TOKEN$/, value: () => 'xapp-1-twinfake', source: 'packages/twin/slack/src/slack-server.ts (bearer /^(xox|xapp-)/)' },
|
|
200
|
-
{ match: /(^|_)SLACK_([A-Z0-9_]*_)?BOT_TOKEN$|(^|_)SLACK_TOKEN$/, value: () => 'xoxb-twinfake', source: 'packages/twin/slack/src/slack-server.ts (bearer /^(xox|xapp-)/); GET /twin identity (any other xoxb-* is the workspace bot)' },
|
|
201
190
|
// tavily-auth.ts and firecrawl-auth.ts refuse a key without the vendor's prefix with the vendor's 401
|
|
202
|
-
{ match: /(^|_)TAVILY_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'tvly-twinfake00000000000000000000', source: '
|
|
203
|
-
{ match: /(^|_)FIRECRAWL_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'fc-twinfake00000000000000000000000', source: '
|
|
204
|
-
|
|
205
|
-
|
|
191
|
+
{ match: /(^|_)TAVILY_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'tvly-twinfake00000000000000000000', source: 'earlier @volter/twin-tavily tavily-auth.ts' },
|
|
192
|
+
{ match: /(^|_)FIRECRAWL_[A-Z0-9_]*(KEY|TOKEN)$/, value: () => 'fc-twinfake00000000000000000000000', source: 'earlier @volter/twin-firecrawl firecrawl-auth.ts FIRECRAWL_KEY_PREFIX' },
|
|
193
|
+
// a Clerk webhook's signing secret is Svix's (`whsec_` and base64), which the app's verifier decodes; the World registers
|
|
194
|
+
// its endpoint with the same secret (the Clerk twin's webhooks door takes `signing_secret`)
|
|
195
|
+
{ match: /(^|_)CLERK_WEBHOOK(_SIGNING)?_SECRET$/, value: () => `whsec_${Buffer.from('twin-fake-clerk-webhook-secret!!').toString('base64')}`, source: 'https://clerk.com/docs/webhooks/sync-data (CLERK_WEBHOOK_SIGNING_SECRET, whsec_); https://docs.svix.com/receiving/verifying-payloads/how-manual' },
|
|
196
|
+
{ match: /(^|_)CLERK_[A-Z0-9_]*SECRET[A-Z0-9_]*$/, value: () => 'sk_test_twinfake000000000000000000000000000000', source: 'earlier @volter/twin-clerk manifest.ts' },
|
|
197
|
+
{ match: /(^|_)CLERK_[A-Z0-9_]*PUBLISHABLE[A-Z0-9_]*$/, value: () => `pk_test_${Buffer.from('twin.clerk.accounts.dev$').toString('base64')}`, source: 'earlier @volter/twin-clerk index.ts' },
|
|
206
198
|
];
|
|
207
199
|
/** Keys the APPLICATION parses before it can use them: a fake that is not the shape its own code decodes throws at the
|
|
208
200
|
* first use, far from any vendor. Each entry names the reader it satisfies; names differ in what they need (cal.com's
|
package/dist/src/host-cli.js
CHANGED
|
@@ -31,11 +31,13 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
31
31
|
const scenarioPath = flags.find((f) => f.startsWith('scenario='))?.slice('scenario='.length);
|
|
32
32
|
// the World's managed database a pack declaring `managedDatabase` is bound to (runtime.ts, managedDatabaseFor)
|
|
33
33
|
const database = flags.find((f) => f.startsWith('database='))?.slice('database='.length);
|
|
34
|
+
// the twin's side ports the runtime allocated (TwinPack.sidePorts): `side.<factory option>=<port>`
|
|
35
|
+
const sideOptions = Object.fromEntries(flags.filter((f) => /^side\.[A-Za-z0-9]+=\d+$/.test(f)).map((f) => { const [k, v] = f.slice('side.'.length).split('='); return [k, Number(v)]; }));
|
|
34
36
|
if (!id || !module || !exportName || !port || !root) {
|
|
35
|
-
process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>][|database=<url>])\n`);
|
|
37
|
+
process.stderr.write(`bad --spec "${raw}" (want id|module|export|port|root[|ro][|scenario=<handlers file>][|database=<url>][|side.<option>=<port>…])\n`);
|
|
36
38
|
process.exit(2);
|
|
37
39
|
}
|
|
38
|
-
specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}), ...(database ? { database } : {}) });
|
|
40
|
+
specs.push({ id, module, export: exportName, port: Number(port), root, readOnly: ro === 'ro', ...(scenarioPath ? { scenarioPath } : {}), ...(database ? { database } : {}), ...(Object.keys(sideOptions).length ? { sideOptions } : {}) });
|
|
39
41
|
}
|
|
40
42
|
if (specs.length === 0) {
|
|
41
43
|
process.stderr.write('no --spec given\n');
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
// POST /fault/exit → process.exit(1) (shared: kills host; worker: kills only that worker)
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
+
import { registerPack } from '@volter/world-core/runtime';
|
|
15
|
+
// The host checks the loaded descriptor before it invokes a factory (architecture, "The catalog"): each twin id the host
|
|
16
|
+
// tests serve with this fixture is a Protocol 3 pack, registered at import in whichever process or worker loads it.
|
|
17
|
+
for (const vendor of ['a', 'b', 'async', 'fixed', 'flaky', 'pending', 'recording', 'rejecting']) {
|
|
18
|
+
registerPack({ vendor, protocol: '3', transport: 'rest', resources: [] });
|
|
19
|
+
}
|
|
14
20
|
export function createFaultTwinServer(options = {}) {
|
|
15
21
|
const root = options.root;
|
|
16
22
|
const countFile = root ? join(root, 'count') : undefined;
|
package/dist/src/host-ready.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
/** Only the host, after all starts have returned and ports were checked, publishes this receipt. */
|
|
2
2
|
export declare function publishHostReady(path: string, boot: string, ports: Record<string, number>): void;
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Receipt admission is cancellation-aware; stale or malformed state is never
|
|
5
|
+
* readiness. `timeoutMs` bounds a host that stops making progress, not a slow
|
|
6
|
+
* one: each change in `progress` (the host log's size, which grows with every
|
|
7
|
+
* twin it starts) begins the bound again, so a host still starting its twins
|
|
8
|
+
* on a busy machine is waited for, and one silent for `timeoutMs` is not.
|
|
9
|
+
*/
|
|
10
|
+
export declare function waitForHostReady(path: string, boot: string, ports: ReadonlyMap<string, number>, timeoutMs: number, abort: AbortSignal, progress?: () => number): Promise<void>;
|
package/dist/src/host-ready.js
CHANGED
|
@@ -9,9 +9,16 @@ export function publishHostReady(path, boot, ports) {
|
|
|
9
9
|
writeFileSync(temporary, `${JSON.stringify({ version: 1, boot, ports })}\n`, { mode: 0o600 });
|
|
10
10
|
renameSync(temporary, path);
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Receipt admission is cancellation-aware; stale or malformed state is never
|
|
14
|
+
* readiness. `timeoutMs` bounds a host that stops making progress, not a slow
|
|
15
|
+
* one: each change in `progress` (the host log's size, which grows with every
|
|
16
|
+
* twin it starts) begins the bound again, so a host still starting its twins
|
|
17
|
+
* on a busy machine is waited for, and one silent for `timeoutMs` is not.
|
|
18
|
+
*/
|
|
19
|
+
export async function waitForHostReady(path, boot, ports, timeoutMs, abort, progress = () => 0) {
|
|
20
|
+
let started = Date.now();
|
|
21
|
+
let seen = progress();
|
|
15
22
|
for (;;) {
|
|
16
23
|
abort.throwIfAborted();
|
|
17
24
|
let bytes;
|
|
@@ -35,8 +42,13 @@ export async function waitForHostReady(path, boot, ports, timeoutMs, abort) {
|
|
|
35
42
|
}
|
|
36
43
|
return;
|
|
37
44
|
}
|
|
45
|
+
const now = progress();
|
|
46
|
+
if (now !== seen) {
|
|
47
|
+
seen = now;
|
|
48
|
+
started = Date.now();
|
|
49
|
+
}
|
|
38
50
|
if (Date.now() - started >= timeoutMs)
|
|
39
|
-
throw new Error(`The co-located host
|
|
51
|
+
throw new Error(`The co-located host made no progress for ${timeoutMs} ms before completing every declared start.`);
|
|
40
52
|
await new Promise((resolve, reject) => {
|
|
41
53
|
const cancel = () => { clearTimeout(timer); abort.removeEventListener('abort', cancel); reject(abort.reason); };
|
|
42
54
|
const timer = setTimeout(() => { abort.removeEventListener('abort', cancel); resolve(); }, Math.min(25, Math.max(1, timeoutMs - (Date.now() - started))));
|
package/dist/src/host-worker.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPack, registerPack } from '@volter/world-core/runtime';
|
|
1
2
|
// Worker entry for 'worker' isolation: one Worker thread per twin. It owns its event
|
|
2
3
|
// loop + heap, so a CPU hog / leak / hard crash here is contained to this thread — the
|
|
3
4
|
// host does NOT restart it: it gives up on the worker (fires a `gaveup` event → doctor
|
|
@@ -6,11 +7,15 @@ import { parentPort, workerData } from 'node:worker_threads';
|
|
|
6
7
|
import { importModule } from "./import-module.js";
|
|
7
8
|
const spec = workerData;
|
|
8
9
|
const mod = await importModule(spec.module);
|
|
10
|
+
const descriptor = mod.pack ?? getPack(spec.id);
|
|
11
|
+
if (!descriptor || descriptor.vendor !== spec.id)
|
|
12
|
+
throw new Error(`Twin ${spec.id}: the loaded module declares no matching pack descriptor`);
|
|
13
|
+
registerPack(descriptor);
|
|
9
14
|
const factory = mod[spec.export];
|
|
10
15
|
if (typeof factory !== 'function') {
|
|
11
16
|
throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
|
|
12
17
|
}
|
|
13
18
|
// Awaited: an async factory (fly) must post `ready` only once its listener is bound.
|
|
14
|
-
const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
|
|
19
|
+
const server = await factory({ ...(spec.sideOptions ?? {}), port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.database !== undefined ? { database: spec.database } : {}) });
|
|
15
20
|
parentPort?.postMessage({ type: 'ready', port: server.port });
|
|
16
21
|
// The Bun.serve listener keeps this worker's event loop alive; nothing else to do.
|
package/dist/src/host.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type ColocatedTwinSpec = {
|
|
|
14
14
|
/** The World's managed database a pack declaring `managedDatabase` serves over (runtime.ts, managedDatabaseFor):
|
|
15
15
|
* the colocated form of the spawn path's `serve <arg> <url>`. */
|
|
16
16
|
database?: string;
|
|
17
|
+
/** The twin's side ports the runtime allocated (TwinPack.sidePorts), as the factory's options (`inspectPort`). */
|
|
18
|
+
sideOptions?: Record<string, number>;
|
|
17
19
|
};
|
|
18
20
|
export type HostIsolation = 'shared' | 'worker';
|
|
19
21
|
export type ColocatedHost = {
|
package/dist/src/host.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPack, registerPack } from '@volter/world-core/runtime';
|
|
1
2
|
// Co-located twin host — run many twins in ONE process instead of one OS process
|
|
2
3
|
// per twin. The twin core is a pure, transport-agnostic handler (see a pack's
|
|
3
4
|
// `handle<Vendor>TwinRequest`), so nothing requires a process per twin; this host
|
|
@@ -26,6 +27,10 @@ import { pathToFileURL } from 'node:url';
|
|
|
26
27
|
import { parallelStartup } from "./startup.js";
|
|
27
28
|
async function loadFactory(spec) {
|
|
28
29
|
const mod = await importModule(spec.module);
|
|
30
|
+
const descriptor = mod.pack ?? getPack(spec.id);
|
|
31
|
+
if (!descriptor || descriptor.vendor !== spec.id)
|
|
32
|
+
throw new Error(`Twin ${spec.id}: the loaded module declares no matching pack descriptor`);
|
|
33
|
+
registerPack(descriptor);
|
|
29
34
|
const factory = mod[spec.export];
|
|
30
35
|
if (typeof factory !== 'function') {
|
|
31
36
|
throw new Error(`Twin "${spec.id}": ${spec.module} has no factory export "${spec.export}"`);
|
|
@@ -82,13 +87,16 @@ export async function startColocatedHost(specs, options = {}) {
|
|
|
82
87
|
try {
|
|
83
88
|
await parallelStartup(specs, async (spec) => {
|
|
84
89
|
const factory = await loadFactory(spec);
|
|
85
|
-
const server = await factory({ port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) });
|
|
90
|
+
const server = await factory({ ...(spec.sideOptions ?? {}), port: spec.port, root: spec.root, readOnly: spec.readOnly, ...(spec.scenarioPath !== undefined ? { scenarioPath: spec.scenarioPath } : {}), ...(spec.database !== undefined ? { database: spec.database } : {}) });
|
|
86
91
|
// Record the handle before validating it, so rollback also owns a bad factory result.
|
|
87
92
|
servers.push(server);
|
|
88
93
|
if (!Number.isInteger(server.port) || server.port < 1 || server.port > 65535 || (spec.port !== 0 && server.port !== spec.port)) {
|
|
89
94
|
throw new Error(`Twin "${spec.id}" bound port ${server.port}, requested ${spec.port}.`);
|
|
90
95
|
}
|
|
91
96
|
ports[spec.id] = server.port;
|
|
97
|
+
// Each start is progress the runtime's readiness wait counts (host-ready.ts); the line says when,
|
|
98
|
+
// in milliseconds since the host process began, since the log's own lines carry no time.
|
|
99
|
+
onEvent({ twin: spec.id, kind: 'started', detail: `port ${server.port} at ${Math.round(performance.now())} ms` });
|
|
92
100
|
});
|
|
93
101
|
}
|
|
94
102
|
catch (error) {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -21,9 +21,9 @@ export { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, inj
|
|
|
21
21
|
export type { CoverageReport, CoverageRow, CoverageStatus, CoverageOptions, RepoVendorSignals } from './covers.js';
|
|
22
22
|
export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from './init.js';
|
|
23
23
|
export { STATE_GITIGNORE } from './init.js';
|
|
24
|
-
export { clockFile } from './runtime.js';
|
|
24
|
+
export { clockFile, instanceDir } from './runtime.js';
|
|
25
25
|
export { checkoutCatalog, findInstalledPackage, installedCatalog, resolveCatalog, resolveServicePackage, twinPackageName, TWIN_PACKAGE_PREFIX, type Catalog, type CatalogKind, packCli, packEntry } from './catalog.js';
|
|
26
|
-
export { MINT_ENV } from './schema.js';
|
|
26
|
+
export { ISSUE_ENV_PREFIX, MINT_ENV } from './schema.js';
|
|
27
27
|
export type { EnvDisposition, InitEnvRow, InitInfraStub, InitOptions, InitPlan, InitResult, InitVendorPlan, InitWiring, } from './init.js';
|
|
28
28
|
export { deriveWorldManifest, findWorldRefFile, isRemoteWorldRef, resolveWorldRef, WORLD_REF_FILE, worldManifest, } from './attach.js';
|
|
29
29
|
export type { ResolvedWorldRef, WorldManifest, WorldRefSource } from './attach.js';
|
package/dist/src/index.js
CHANGED
|
@@ -19,9 +19,9 @@ export { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, inj
|
|
|
19
19
|
// world config + env file OUTSIDE the repo (fixture-env), and prove the emission with `covers`.
|
|
20
20
|
export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from "./init.js";
|
|
21
21
|
export { STATE_GITIGNORE } from "./init.js";
|
|
22
|
-
export { clockFile } from "./runtime.js";
|
|
22
|
+
export { clockFile, instanceDir } from "./runtime.js";
|
|
23
23
|
export { checkoutCatalog, findInstalledPackage, installedCatalog, resolveCatalog, resolveServicePackage, twinPackageName, TWIN_PACKAGE_PREFIX, packCli, packEntry } from "./catalog.js";
|
|
24
|
-
export { MINT_ENV } from "./schema.js";
|
|
24
|
+
export { ISSUE_ENV_PREFIX, MINT_ENV } from "./schema.js";
|
|
25
25
|
export { deriveWorldManifest, findWorldRefFile, isRemoteWorldRef, resolveWorldRef, WORLD_REF_FILE, worldManifest, } from "./attach.js";
|
|
26
26
|
export { parseSni, readReflectRoutes, reflectRoutesPath, reflectManifestPath, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, CA_TRUST_ENV, ATTACHED_CA_PATH, startReflectFront, startReflectResolver, writeReflectRoutes, } from "./reflect.js";
|
|
27
27
|
export { startColocatedHost } from "./host.js";
|