@volter/world-runtime 2.0.55 → 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/schema.d.ts
CHANGED
|
@@ -65,6 +65,13 @@ export type WorldManifestV2 = {
|
|
|
65
65
|
};
|
|
66
66
|
/** The env value `init` writes for a name whose fake must be minted per instance (a service-account JSON with a real throwaway key). */
|
|
67
67
|
export declare const MINT_ENV = "$mint";
|
|
68
|
+
/** An env value `$issue:<service id>`: the credential that service's twin issues through its descriptor's
|
|
69
|
+
* `credentialDoor`, asked for at boot once the twin is up (never committed, like `$mint`). */
|
|
70
|
+
export declare const ISSUE_ENV_PREFIX = "$issue:";
|
|
71
|
+
/** The env value `init` writes for the application's own origin (its base URL, its auth URL): resolved when a command
|
|
72
|
+
* attaches, from the application URL the booter recorded (`volter-world app-url <world> --set`), so the app names
|
|
73
|
+
* the address it is reached at, which only its booter knows (a browser tab's preview origin, a local port). */
|
|
74
|
+
export declare const APP_URL_ENV = "$app-url";
|
|
68
75
|
export type WorldShareProvider = 'cloudflare-quick' | 'command';
|
|
69
76
|
/**
|
|
70
77
|
* EXTERNAL (self-managed) service type.
|
|
@@ -103,6 +110,9 @@ export type WorldExternalDiscoverSource = 'status' | 'up';
|
|
|
103
110
|
*/
|
|
104
111
|
export type WorldExternalDiscover = {
|
|
105
112
|
as: string;
|
|
113
|
+
/** The wire the discovered connection speaks (`postgres`, `mysql`, `mongodb`, `redis`), so a coverage report binds it
|
|
114
|
+
* to the application's driver instead of calling it unknown. */
|
|
115
|
+
protocol?: string;
|
|
106
116
|
source?: WorldExternalDiscoverSource;
|
|
107
117
|
jsonPath?: string;
|
|
108
118
|
pattern?: string;
|
|
@@ -159,6 +169,14 @@ export type WorldServiceConfig = {
|
|
|
159
169
|
'//'?: string;
|
|
160
170
|
id: string;
|
|
161
171
|
type?: WorldServiceType;
|
|
172
|
+
/** The credential this twin issues the World's applications at boot (its pack's `credentialDoor`, copied by `init`):
|
|
173
|
+
* `up` POSTs `body` to `path` on the running twin and sets each name of `fill` whose value is `$issue:<this id>` to
|
|
174
|
+
* the answer's field `fill` names for it. */
|
|
175
|
+
issues?: {
|
|
176
|
+
path: string;
|
|
177
|
+
body: Record<string, unknown>;
|
|
178
|
+
fill: Record<string, string>;
|
|
179
|
+
};
|
|
162
180
|
/**
|
|
163
181
|
* The twin PACKAGE this service boots from (`@volter/twin-<vendor>`), resolved from the world
|
|
164
182
|
* root at boot by node's own `node_modules` walk (docs/concepts/worlds.md#the-config-and-the-running-world).
|
|
@@ -191,11 +209,6 @@ export type WorldServiceConfig = {
|
|
|
191
209
|
* to loopback services directly while presenting a disguised world to the app/user.
|
|
192
210
|
*/
|
|
193
211
|
controlPlane?: boolean;
|
|
194
|
-
/** The account a vendor's own screens (its mirror) open signed in as: its handle, username or email, which the pack
|
|
195
|
-
* resolves against the accounts this World holds (`<vendor>MirrorSignIn`). Nothing secret is written here. */
|
|
196
|
-
signIn?: {
|
|
197
|
-
as: string;
|
|
198
|
-
};
|
|
199
212
|
injectEnv?: string;
|
|
200
213
|
/**
|
|
201
214
|
* Additional env vars exported into the generated world env after this service starts.
|
|
@@ -337,6 +350,11 @@ export type WorldServiceInstance = {
|
|
|
337
350
|
port?: number;
|
|
338
351
|
/** Absent for 'external' services — their URL is discovered into env vars, not assigned by us. */
|
|
339
352
|
url?: string;
|
|
353
|
+
/** The twin's side ports (TwinPack.sidePorts: the SMTP twin's mailbox), by name: `volter-world url <world> <id>.<name>`. */
|
|
354
|
+
sides?: Record<string, {
|
|
355
|
+
port: number;
|
|
356
|
+
url: string;
|
|
357
|
+
}>;
|
|
340
358
|
/** 0 for 'external' services — they have no process we own (the external tool manages it). */
|
|
341
359
|
pid: number;
|
|
342
360
|
log: string;
|
|
@@ -454,6 +472,10 @@ export type WorldInstance = {
|
|
|
454
472
|
clonedAt?: string;
|
|
455
473
|
fetchedAt?: string;
|
|
456
474
|
};
|
|
475
|
+
/** The facts files of the packs this World resolved from installed packages (each released pack carries its own,
|
|
476
|
+
* `generated/pack-facts.json`): the kernel's pack-facts loader overlays them on the facts it was built with
|
|
477
|
+
* (docs/contributing/architecture.md, "The catalog: where twins come from"). */
|
|
478
|
+
packFacts?: string[];
|
|
457
479
|
};
|
|
458
480
|
export declare const DEFAULT_WORLD_SELECTION: WorldSelection;
|
|
459
481
|
export declare const LEGACY_WORLD_SELECTION: WorldSelection;
|
package/dist/src/schema.js
CHANGED
|
@@ -3,6 +3,13 @@ export const WORLD_SCHEMA_VERSION = 2;
|
|
|
3
3
|
export const WORLD_USAGES = ['application', 'dependencies', 'build', 'deployment'];
|
|
4
4
|
/** The env value `init` writes for a name whose fake must be minted per instance (a service-account JSON with a real throwaway key). */
|
|
5
5
|
export const MINT_ENV = '$mint';
|
|
6
|
+
/** An env value `$issue:<service id>`: the credential that service's twin issues through its descriptor's
|
|
7
|
+
* `credentialDoor`, asked for at boot once the twin is up (never committed, like `$mint`). */
|
|
8
|
+
export const ISSUE_ENV_PREFIX = '$issue:';
|
|
9
|
+
/** The env value `init` writes for the application's own origin (its base URL, its auth URL): resolved when a command
|
|
10
|
+
* attaches, from the application URL the booter recorded (`volter-world app-url <world> --set`), so the app names
|
|
11
|
+
* the address it is reached at, which only its booter knows (a browser tab's preview origin, a local port). */
|
|
12
|
+
export const APP_URL_ENV = '$app-url';
|
|
6
13
|
function assertStringArray(value, what, path) {
|
|
7
14
|
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string')) {
|
|
8
15
|
throw new Error(`${what} must be a non-empty string array in ${path}`);
|
|
@@ -74,10 +81,13 @@ function assertExternalServiceConfig(service, path) {
|
|
|
74
81
|
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) {
|
|
75
82
|
throw new Error(`External service "${id}" discover entries must be objects in ${path}`);
|
|
76
83
|
}
|
|
77
|
-
assertKeys(mapping, ['as', 'source', 'jsonPath', 'pattern'], `External service "${id}" discover entry`, path);
|
|
84
|
+
assertKeys(mapping, ['as', 'protocol', 'source', 'jsonPath', 'pattern'], `External service "${id}" discover entry`, path);
|
|
78
85
|
if (!mapping.as || typeof mapping.as !== 'string') {
|
|
79
86
|
throw new Error(`External service "${id}" discover entry must define string "as" in ${path}`);
|
|
80
87
|
}
|
|
88
|
+
if (mapping.protocol !== undefined && !['postgres', 'mysql', 'mongodb', 'redis', 'http'].includes(mapping.protocol)) {
|
|
89
|
+
throw new Error(`External service "${id}" discover "${mapping.as}" protocol must be postgres, mysql, mongodb, redis or http in ${path}`);
|
|
90
|
+
}
|
|
81
91
|
const hasJson = typeof mapping.jsonPath === 'string';
|
|
82
92
|
const hasPattern = typeof mapping.pattern === 'string';
|
|
83
93
|
if (hasJson === hasPattern) {
|
|
@@ -360,7 +370,7 @@ export function assertWorldSelection(value, path) {
|
|
|
360
370
|
function serviceV2ToLegacy(value, path) {
|
|
361
371
|
if (!record(value))
|
|
362
372
|
throw new Error(`World service must be an object in ${path}`);
|
|
363
|
-
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root'
|
|
373
|
+
assertKeys(value, ['id', 'type', 'description', 'source', 'execution', 'endpoint', 'bindings', 'root'], `Service ${String(value.id ?? '')}`, path, true);
|
|
364
374
|
const source = value.source === undefined ? {} : value.source;
|
|
365
375
|
const execution = value.execution === undefined ? {} : value.execution;
|
|
366
376
|
const endpoint = value.endpoint === undefined ? {} : value.endpoint;
|
|
@@ -381,13 +391,6 @@ function serviceV2ToLegacy(value, path) {
|
|
|
381
391
|
throw new Error(`World service must define string id in ${path}`);
|
|
382
392
|
if (value.description !== undefined && typeof value.description !== 'string')
|
|
383
393
|
throw new Error(`Service "${value.id}" description must be a string in ${path}`);
|
|
384
|
-
if (value.signIn !== undefined) {
|
|
385
|
-
if (!record(value.signIn))
|
|
386
|
-
throw new Error(`Service "${value.id}" signIn must be an object in ${path}`);
|
|
387
|
-
assertKeys(value.signIn, ['as'], `Service "${value.id}" signIn`, path);
|
|
388
|
-
if (typeof value.signIn.as !== 'string' || value.signIn.as.trim() === '')
|
|
389
|
-
throw new Error(`Service "${value.id}" signIn.as must name an account (a handle, username or email) in ${path}`);
|
|
390
|
-
}
|
|
391
394
|
if (value.description !== undefined && value['//'] !== undefined)
|
|
392
395
|
throw new Error(`Service "${value.id}" must not set both description and // in ${path}`);
|
|
393
396
|
if (execution.cwd !== undefined && typeof execution.cwd !== 'string')
|
|
@@ -412,7 +415,7 @@ function serviceV2ToLegacy(value, path) {
|
|
|
412
415
|
for (const mapping of bindings.discover) {
|
|
413
416
|
if (!record(mapping))
|
|
414
417
|
throw new Error(`Service "${value.id}" bindings.discover entries must be objects in ${path}`);
|
|
415
|
-
assertKeys(mapping, ['as', 'source', 'jsonPath', 'pattern'], `Service "${value.id}" bindings.discover entry`, path);
|
|
418
|
+
assertKeys(mapping, ['as', 'protocol', 'source', 'jsonPath', 'pattern'], `Service "${value.id}" bindings.discover entry`, path);
|
|
416
419
|
}
|
|
417
420
|
}
|
|
418
421
|
const type = value.type;
|
|
@@ -446,7 +449,6 @@ function serviceV2ToLegacy(value, path) {
|
|
|
446
449
|
...(bindings.injectEnvTemplates === undefined ? {} : { injectEnvTemplates: bindings.injectEnvTemplates }),
|
|
447
450
|
...(bindings.cliRedirect === undefined ? {} : { cliRedirect: bindings.cliRedirect }),
|
|
448
451
|
...(value.root === undefined ? {} : { root: value.root }),
|
|
449
|
-
...(value.signIn === undefined ? {} : { signIn: { as: value.signIn.as } }),
|
|
450
452
|
...(external === undefined ? {} : { external }),
|
|
451
453
|
};
|
|
452
454
|
if (type !== 'external' && bindings.discover !== undefined)
|
|
@@ -584,7 +586,6 @@ function serviceToV2(service) {
|
|
|
584
586
|
...(Object.keys(endpoint).length ? { endpoint } : {}),
|
|
585
587
|
...(Object.keys(bindings).length ? { bindings } : {}),
|
|
586
588
|
...(service.root === undefined ? {} : { root: service.root }),
|
|
587
|
-
...(service.signIn === undefined ? {} : { signIn: service.signIn }),
|
|
588
589
|
};
|
|
589
590
|
}
|
|
590
591
|
/** Serialize normalized intent as the committed format-2 manifest. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type HistoryReference } from '@volter/world-core';
|
|
2
|
-
import { type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core';
|
|
1
|
+
import { type HistoryReference } from '@volter/world-core/runtime';
|
|
2
|
+
import { type TwinStreamConnection, type TwinStreamSink } from '@volter/world-core/runtime';
|
|
3
3
|
import { type TrustedIssuer } from '@volter/world-access';
|
|
4
|
-
import { type Changeset, type Receipt } from '@volter/world-core';
|
|
4
|
+
import { type Changeset, type Receipt } from '@volter/world-core/runtime';
|
|
5
5
|
import { rootForControlRoot } from './root.js';
|
|
6
6
|
import { type WorldInstance } from './schema.js';
|
|
7
7
|
export declare const TOKEN_HEADER = "x-volter-token";
|
|
@@ -101,7 +101,7 @@ export type DoorHost = {
|
|
|
101
101
|
reload: () => WorldLayout;
|
|
102
102
|
/** The twin's wire for a request whose path is the twin's own; null when the world has no such twin. */
|
|
103
103
|
twinFetch: (vendor: string) => ((request: Request) => Promise<Response>) | null;
|
|
104
|
-
/** The world's byte-stream doors (a TCP protocol a twin serves: smtp's
|
|
104
|
+
/** The world's byte-stream doors (a TCP protocol a twin serves: smtp's),
|
|
105
105
|
* by the id a local World gives that listener's service, when this host carries bytes over a
|
|
106
106
|
* WebSocket (a hosted World takes no inbound TCP). */
|
|
107
107
|
streams?: () => Record<string, {
|
|
@@ -111,22 +111,6 @@ export type DoorHost = {
|
|
|
111
111
|
/** Where this host reads a twin's scenario (the handlers document a generative twin serves from,
|
|
112
112
|
* re-read on every request), or null when the twin takes none here. */
|
|
113
113
|
scenarioPath?: (vendor: string) => string | null;
|
|
114
|
-
/** A twin's mirror as its pack builds it: `index.html` (the shell) or `assets/<file>`, or null when
|
|
115
|
-
* the twin has none here. Every host answers the same mount (`/<served>/<vendor>/mirror/`); only
|
|
116
|
-
* where the files come from differs (built at serve under Bun, prebuilt under Node, static assets
|
|
117
|
-
* on Cloudflare). */
|
|
118
|
-
mirror?: (vendor: string, file: string) => Promise<Response | null>;
|
|
119
|
-
/** Whether the twin has a mirror on this host, without building it. */
|
|
120
|
-
hasMirror?: (vendor: string) => boolean | Promise<boolean>;
|
|
121
|
-
/** The account and token a twin's screens open signed in as, resolved by its pack from the twin's state at `root`
|
|
122
|
-
* for the account the World's config names (`signIn.as`); null when the pack cannot, or has no such account. */
|
|
123
|
-
mirrorSignIn?: (vendor: string, as: string, ctx: {
|
|
124
|
-
root: string;
|
|
125
|
-
twin: (path: string, init?: RequestInit) => Promise<Response>;
|
|
126
|
-
}) => Promise<{
|
|
127
|
-
account: string;
|
|
128
|
-
token: string;
|
|
129
|
-
} | null>;
|
|
130
114
|
/** Where this World's clock lives in its store (the file every twin reads `worldNow()` from). */
|
|
131
115
|
clockPath?: () => string;
|
|
132
116
|
/** The origin a browser reaches this World at (`http://<world>--<org>.localhost:<port>`), when the
|
|
@@ -267,7 +251,7 @@ export declare class WorldDoors {
|
|
|
267
251
|
private scope;
|
|
268
252
|
/** The request's scope, whether it came by a token the caller presented or by a browser's session
|
|
269
253
|
* (the cookie `POST …/session` set), and the token that opened it. Every form presented is a
|
|
270
|
-
* candidate: a
|
|
254
|
+
* candidate: a screen's page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
|
|
271
255
|
* session cookie, and the cookie is what opens the World. With a browser origin of its own
|
|
272
256
|
* (`browserOrigin`), a World honours its session only on that origin: a session cookie on any other
|
|
273
257
|
* (a host's shared origin, path-addressed) opens nothing. */
|
|
@@ -308,18 +292,10 @@ export declare class WorldDoors {
|
|
|
308
292
|
private controlRoot;
|
|
309
293
|
private stateOf;
|
|
310
294
|
handle(request: Request): Promise<Response>;
|
|
311
|
-
/**
|
|
312
|
-
*
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
* shell. A link is not a twin and has no mirror. */
|
|
316
|
-
private mirror;
|
|
317
|
-
/** A vendor's screens' sign-in (`signIn.as`), once per account every twelve hours while this World is served (rotating the
|
|
318
|
-
* tokens forgets them): the pack finds the account and mints its credential through the twin's own doors; a failure
|
|
319
|
-
* is not kept, so the next page asks again. An account or credential that is not plain printable text is refused. */
|
|
320
|
-
private readonly signIns;
|
|
321
|
-
private readonly signInAt;
|
|
322
|
-
private signedIn;
|
|
295
|
+
/** A twin's workspaces as its discovery door lists them (`GET /twin`'s `screens`: each built workspace screen's id and
|
|
296
|
+
* path), which a person is shown at `/<served>/<vendor><path>`. Asked again after a minute, as `enforcesRead` is. */
|
|
297
|
+
private readonly workspaces;
|
|
298
|
+
private screensOf;
|
|
323
299
|
/** Whether a twin enforces a read-scope request itself (its manifest names `requestScopes: ['read']`):
|
|
324
300
|
* only then may the read token reach it with anything but GET or HEAD. Asked once per twin. */
|
|
325
301
|
private readonly readEnforced;
|
|
@@ -342,7 +318,7 @@ export declare class WorldDoors {
|
|
|
342
318
|
/** The world's own doors under `/-/<org>/<world>/`. */
|
|
343
319
|
private door;
|
|
344
320
|
/** A browser session for this World: the presented token as an HttpOnly cookie scoped to the World's
|
|
345
|
-
* paths, so a page served under it (a
|
|
321
|
+
* paths, so a page served under it (a vendor's screen) reads the wire as the token's holder, in the
|
|
346
322
|
* token's scope. */
|
|
347
323
|
/** THE LOCAL SESSION: a World served on this machine's loopback never asks the person at it for a token. Any
|
|
348
324
|
* website can send requests to 127.0.0.1, so what stands in for the token is proof the request is this World's own
|
|
@@ -364,7 +340,7 @@ export declare class WorldDoors {
|
|
|
364
340
|
private branchesDoor;
|
|
365
341
|
/** The newest entry's instant across the World's twins (ms), or null when it has none. */
|
|
366
342
|
private newestEntry;
|
|
367
|
-
/** THE MAP: every twin and what it holds, by resource type, and
|
|
343
|
+
/** THE MAP: every twin and what it holds, by resource type, and the workspaces it shows. */
|
|
368
344
|
private map;
|
|
369
345
|
/** THE TIMELINE: the entries of every twin's log merged, newest first, by each entry's instant, then
|
|
370
346
|
* twin, then position. The World keeps no order across twins finer than its clock, and this claims
|
package/dist/src/served-world.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getActiveWorldStore,
|
|
1
|
+
import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant, historyDigest, historyEntries, readHistoryView, worldPaths } from '@volter/world-core/runtime';
|
|
2
2
|
// A SERVED WORLD (docs/reference/http-api.md#a-served-world): `volter world serve` puts a world at
|
|
3
3
|
// `http://<host>:<port>/<org>/<world>/`. Each twin's vendor API is under its vendor id — the
|
|
4
4
|
// addressed grammar RH2 depends on — and the world's own doors are under `/-/<org>/<world>/`:
|
|
@@ -19,24 +19,25 @@ import { getActiveWorldStore, withAncestryLock, captureHistory, historyAtInstant
|
|
|
19
19
|
// GET/PUT clock, POST clock/advance the frozen instant every twin stamps from, forward only
|
|
20
20
|
// GET/POST branches, DELETE branches/<org>/<world> the host's branches of this World ("as of" views)
|
|
21
21
|
// GET history?at=<instant> each twin's history cut at an instant (where such a branch starts)
|
|
22
|
-
// GET /<org>/<world>/<vendor
|
|
22
|
+
// GET /<org>/<world>/<vendor>/<path> the vendor's own screens, the pack's workspaces (its `GET /twin` lists them)
|
|
23
23
|
// Tokens ride `x-volter-token`: the token opens everything, the read token only reads — on the vendor
|
|
24
24
|
// wire, a twin that enforces it (`requestScopes: ['read']`) is handed the read token's other requests
|
|
25
25
|
// marked read-only and refuses the writes itself. `serve` mints both, prints them once and writes them
|
|
26
26
|
// to `.volter/token` and `.volter/token.read` (0600).
|
|
27
27
|
// One writer per world by construction: the doors run in this process, serialized per twin.
|
|
28
28
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
29
|
-
import { CALLER_HEADER, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf,
|
|
29
|
+
import { CALLER_HEADER, callerForHeader, newTraceparent, parseTraceparent, traceparentForDelivery, readTwinRequestJournalFrom, assertSafeChangesetName, blobDigest, destinationOf, getActiveBlobStore, journalTwinRequest, loadCheck, twinRequestJournalFailures, openSealedAt, readTwinRequestJournal, serveHttp, twinRequestCredentials, TWIN_PREFIX_HEADER, validateRemoteOrigin } from '@volter/world-core/runtime';
|
|
30
30
|
import { APP_READ_ENDPOINT_ENV } from "./init.js";
|
|
31
|
+
import { packFacts } from "./pack-facts.js";
|
|
31
32
|
import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass } from '@volter/world-access';
|
|
32
33
|
import { createHash } from 'node:crypto';
|
|
33
34
|
import { dirname, join, resolve } from 'node:path';
|
|
34
|
-
import { appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, changesetReadiness, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow, parentEntries, readBranchMeta, rebaseBranch, } from '@volter/world-core';
|
|
35
|
+
import { appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, changesetReadiness, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow, parentEntries, readBranchMeta, rebaseBranch, } from '@volter/world-core/runtime';
|
|
35
36
|
import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from "./changeset.js";
|
|
36
37
|
import { fetchFromOrigin } from "./origin.js";
|
|
37
38
|
import { CONSOLE_BASE, consoleRedirect, serveConsoleApart } from "./console-apart.js";
|
|
38
39
|
import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from "./configs.js";
|
|
39
|
-
import { adaptersFor, credentialPath,
|
|
40
|
+
import { adaptersFor, credentialPath, credentialPayloadFrom, deployTwin, deployWorld, loadWorldChecks, twinSigningSecret, materializeRoots, refreshPosture, refreshTwin, rootForControlRoot, rewrapTwinCredentials, sealTwinCredential, sealedCredentialInfo, bindLegacyCredentials, setTwinRoot, spanMs } from "./root.js";
|
|
40
41
|
import { clockFile, downWorld, listWorlds, statusWorld, upWorld } from "./runtime.js";
|
|
41
42
|
import { assertChangesetRules } from "./schema.js";
|
|
42
43
|
export const TOKEN_HEADER = 'x-volter-token';
|
|
@@ -131,15 +132,6 @@ export async function mountWorld(name, opts = {}) {
|
|
|
131
132
|
const declared = loadWorldConfig(configRef, worldRoot).config.services.find((s) => s.id === vendor)?.colocate?.scenarioPath;
|
|
132
133
|
return declared ? resolve(worldRoot, declared) : null;
|
|
133
134
|
},
|
|
134
|
-
// a local mirror is the pack's own, built by its builder (at serve under Bun, prebuilt under Node)
|
|
135
|
-
mirror: (vendor, file) => localMirrorFile(worldRoot, vendor, file),
|
|
136
|
-
hasMirror: async (vendor) => (await localMirror(worldRoot, vendor)) !== null,
|
|
137
|
-
mirrorSignIn: async (vendor, as, ctx) => { const mirror = await localMirror(worldRoot, vendor); try {
|
|
138
|
-
return (await mirror?.signIn?.(as, ctx)) ?? null;
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return null;
|
|
142
|
-
} },
|
|
143
135
|
clockPath: () => clockFile(worldRoot, name),
|
|
144
136
|
...(opts.branches?.(served) ? { branches: opts.branches(served) } : {}),
|
|
145
137
|
...(opts.browserOrigin ? { browserOrigin: () => opts.browserOrigin(served), originOf: (other) => opts.browserOrigin(other) } : {}),
|
|
@@ -151,7 +143,9 @@ export async function mountWorld(name, opts = {}) {
|
|
|
151
143
|
const target = instance?.services[vendor]?.url;
|
|
152
144
|
if (!target)
|
|
153
145
|
return null;
|
|
154
|
-
|
|
146
|
+
// the body as bytes, none when empty: an empty body streamed is sent chunked with no last chunk (Bun 1.2), and the
|
|
147
|
+
// twin's server reads the next request on the connection as its continuation (HPE_INVALID_CHUNK_SIZE)
|
|
148
|
+
return async (request) => { const at = new URL(request.url); const body = request.body ? new Uint8Array(await request.arrayBuffer()) : undefined; return fetch(`${target.replace(/\/+$/, '')}${at.pathname}${at.search}`, { method: request.method, headers: request.headers, ...(body?.byteLength ? { body } : {}), redirect: 'manual' }); };
|
|
155
149
|
},
|
|
156
150
|
};
|
|
157
151
|
const world = new WorldDoors(name, worldRoot, configRef, served, host, held('token') ?? mintToken('tok_'), held('token.read') ?? mintToken('tok_r_'));
|
|
@@ -239,37 +233,6 @@ export async function mountWorld(name, opts = {}) {
|
|
|
239
233
|
}
|
|
240
234
|
return mounted;
|
|
241
235
|
}
|
|
242
|
-
/** A local World's mirrors: each pack's builders, found once per process, and each client built once. */
|
|
243
|
-
const localMirrors = new Map();
|
|
244
|
-
const builtMirrorFiles = new Map();
|
|
245
|
-
function localMirror(worldRoot, vendor) {
|
|
246
|
-
const key = `${worldRoot}\0${vendor}`;
|
|
247
|
-
let held = localMirrors.get(key);
|
|
248
|
-
if (!held) {
|
|
249
|
-
held = packMirror(vendor, worldRoot).catch(() => null);
|
|
250
|
-
localMirrors.set(key, held);
|
|
251
|
-
}
|
|
252
|
-
return held;
|
|
253
|
-
}
|
|
254
|
-
async function localMirrorFile(worldRoot, vendor, file) {
|
|
255
|
-
const mirror = await localMirror(worldRoot, vendor);
|
|
256
|
-
if (!mirror)
|
|
257
|
-
return null;
|
|
258
|
-
if (file === 'index.html')
|
|
259
|
-
return new Response(mirror.html(), { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
260
|
-
const build = file === 'assets/app.js' ? mirror.client : file === 'assets/styles.css' ? mirror.styles : undefined;
|
|
261
|
-
if (!build)
|
|
262
|
-
return null;
|
|
263
|
-
const key = `${worldRoot}\0${vendor}\0${file}`;
|
|
264
|
-
let held = builtMirrorFiles.get(key);
|
|
265
|
-
// a failed build is not kept: the next request builds again
|
|
266
|
-
if (!held) {
|
|
267
|
-
held = build().catch(() => { builtMirrorFiles.delete(key); return null; });
|
|
268
|
-
builtMirrorFiles.set(key, held);
|
|
269
|
-
}
|
|
270
|
-
const body = await held;
|
|
271
|
-
return body === null ? null : new Response(body, { headers: { 'content-type': file.endsWith('.css') ? 'text/css; charset=utf-8' : 'text/javascript; charset=utf-8' } });
|
|
272
|
-
}
|
|
273
236
|
/** Boot the world (state kept) and serve it on its own port. Announces and returns after boot; `stop` downs the world. */
|
|
274
237
|
export async function serveWorld(name, opts = {}) {
|
|
275
238
|
const mounted = await mountWorld(name, opts);
|
|
@@ -362,7 +325,6 @@ export function worldOriginLabel(served) {
|
|
|
362
325
|
/** The header the doors set on a read-scope request to a twin: the twin refuses the caller's writes
|
|
363
326
|
* and still makes the vendor's own moves. It only restricts, so a twin trusts it without a token. */
|
|
364
327
|
export const READ_ONLY_HEADER = 'x-volter-read-only';
|
|
365
|
-
const MIRROR_ASSET = /^assets\/[A-Za-z0-9._-]+$/;
|
|
366
328
|
const SPAN = /^(\d+(?:\.\d+)?)(s|m|h|d)$/;
|
|
367
329
|
const SPAN_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
368
330
|
/** A W3C traceparent's trace id, or null when the value is not one. */
|
|
@@ -399,7 +361,7 @@ export class WorldDoors {
|
|
|
399
361
|
}
|
|
400
362
|
/** New tokens; a request presenting the old ones is refused from the next call on. */
|
|
401
363
|
// new tokens end everything the old ones reached: every session and every named key
|
|
402
|
-
retoken(token, readToken) { this.token = token; this.readToken = readToken; this.endSessions(); this.keepKeys([]);
|
|
364
|
+
retoken(token, readToken) { this.token = token; this.readToken = readToken; this.endSessions(); this.keepKeys([]); }
|
|
403
365
|
/** BROWSER SESSIONS: the session cookie carries an opaque id, never a token, so a leaked cookie is one session and
|
|
404
366
|
* not the World's write token; rotating the tokens ends every session. Kept in the World's store, so a restart
|
|
405
367
|
* keeps them; each lasts 30 days from its opening. */
|
|
@@ -635,7 +597,7 @@ export class WorldDoors {
|
|
|
635
597
|
scope(request) { return this.credential(request).scope; }
|
|
636
598
|
/** The request's scope, whether it came by a token the caller presented or by a browser's session
|
|
637
599
|
* (the cookie `POST …/session` set), and the token that opened it. Every form presented is a
|
|
638
|
-
* candidate: a
|
|
600
|
+
* candidate: a screen's page sends the VENDOR's bearer (the token its sign-in holds) beside the World's
|
|
639
601
|
* session cookie, and the cookie is what opens the World. With a browser origin of its own
|
|
640
602
|
* (`browserOrigin`), a World honours its session only on that origin: a session cookie on any other
|
|
641
603
|
* (a host's shared origin, path-addressed) opens nothing. */
|
|
@@ -807,15 +769,21 @@ export class WorldDoors {
|
|
|
807
769
|
// its env is what the client reads, with ${host}/${port} the bridge's listener
|
|
808
770
|
const ws = url.origin.replace(/^http/, 'ws');
|
|
809
771
|
const streams = Object.fromEntries(Object.entries(this.host.streams?.() ?? {}).map(([id, { protocol }]) => [id, { protocol, url: `${ws}/-/${this.served}/streams/${id}`, env: streamEnv(id, protocol) }]));
|
|
810
|
-
//
|
|
811
|
-
|
|
772
|
+
// each twin's endpoint variables as its descriptor declares them (`endpointEnv`, or the app-read table's), at its
|
|
773
|
+
// place here: the name, and each template on its URL (a template on a listener's host and port is a stream's)
|
|
774
|
+
const endpoints = Object.fromEntries(Object.keys(vendors).flatMap((v) => {
|
|
775
|
+
const declared = packFacts()[v]?.endpointEnv;
|
|
776
|
+
const name = declared?.name ?? APP_READ_ENDPOINT_ENV[v]?.injectEnv;
|
|
777
|
+
const templates = declared?.templates ?? APP_READ_ENDPOINT_ENV[v]?.injectEnvTemplates ?? {};
|
|
778
|
+
return [
|
|
779
|
+
...(name ? [[name, vendors[v]]] : []),
|
|
780
|
+
...Object.entries(templates).filter(([, t]) => !/\$\{(?:host|port)\}/.test(t)).map(([n, t]) => [n, t.replaceAll('${url}', vendors[v])]),
|
|
781
|
+
];
|
|
782
|
+
}));
|
|
812
783
|
return Response.json({ name: this.served, vendors, ca: null, proxy: null, env: { ...endpoints, VOLTER_TWINS_KEY: presented }, ...(Object.keys(streams).length ? { streams } : {}) });
|
|
813
784
|
}
|
|
814
785
|
if (url.pathname.startsWith(prefix)) {
|
|
815
786
|
const rest = url.pathname.slice(prefix.length);
|
|
816
|
-
const mirror = /^([a-z0-9_-]+)\/mirror(?:\/(.*))?$/.exec(rest);
|
|
817
|
-
if (mirror)
|
|
818
|
-
return await this.mirror(request, url, mirror[1], mirror[2] ?? '');
|
|
819
787
|
return await this.wire(request, url, rest);
|
|
820
788
|
}
|
|
821
789
|
return Response.json({ error: `not this world: serving /${this.served}/` }, { status: 404 });
|
|
@@ -824,71 +792,26 @@ export class WorldDoors {
|
|
|
824
792
|
return Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
|
825
793
|
}
|
|
826
794
|
}
|
|
827
|
-
/**
|
|
828
|
-
*
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (rest === '' && !atSite && ['document', 'iframe'].includes(request.headers.get('sec-fetch-dest') ?? ''))
|
|
848
|
-
return new Response(null, { status: 302, headers: { location: `/${this.served}/${vendor}/${url.search}` } });
|
|
849
|
-
// the shell's <base> is the twin's place under this World (its reads go to the World's wire), its assets stay under
|
|
850
|
-
// mirror/, and its `#/…` links keep working under that base (world-core mirror-shell.ts)
|
|
851
|
-
let html = mirrorShellUnder(await file.text(), `/${this.served}/${vendor}/`);
|
|
852
|
-
// SIGNED IN AS the config says (`signIn.as`): the screens are handed the account and a credential the World minted
|
|
853
|
-
// for it, before they start. Only to a request that may write this World (a read link signs no one in, and minting
|
|
854
|
-
// is a write), on the vendor's site (never the keyless mount), at the World's own origin: a shared origin's other
|
|
855
|
-
// pages, and a rebound name reaching this port, are not this World's pages. Checked here, whoever called.
|
|
856
|
-
const own = this.host.browserOrigin?.() ?? null;
|
|
857
|
-
const signs = atSite && own !== null && url.host === new URL(own).host && this.credential(request).scope === 'write';
|
|
858
|
-
const as = signs ? loadWorldConfig(this.configRef, this.worldRoot).config.services.find((s) => s.id === vendor)?.signIn?.as : undefined;
|
|
859
|
-
const given = as ? await this.signedIn(vendor, as, url.origin) : null;
|
|
860
|
-
if (given) {
|
|
861
|
-
// data, never markup: every character that could end the script or the line is escaped, and the replacement is a
|
|
862
|
-
// function, so no `$&` or `$'` in a value is expanded into the page
|
|
863
|
-
const data = JSON.stringify({ account: given.account, token: given.token }).replace(/[<>&\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
864
|
-
html = html.replace(/<script/i, (tag) => `<script>window.__VOLTER_SIGN_IN__=${data};</script>${tag}`);
|
|
865
|
-
}
|
|
866
|
-
// where the console lives on the World's own origin, only that origin frames the World's pages (no clickjacking);
|
|
867
|
-
// a page that carries a credential is never stored, and differs by who asked
|
|
868
|
-
return new Response(html, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': given ? 'no-store, private' : 'no-cache', vary: given ? 'Sec-Fetch-Dest, Cookie, Authorization' : 'Sec-Fetch-Dest', ...(own ? { 'content-security-policy': "frame-ancestors 'self'" } : {}) } });
|
|
869
|
-
}
|
|
870
|
-
/** A vendor's screens' sign-in (`signIn.as`), once per account every twelve hours while this World is served (rotating the
|
|
871
|
-
* tokens forgets them): the pack finds the account and mints its credential through the twin's own doors; a failure
|
|
872
|
-
* is not kept, so the next page asks again. An account or credential that is not plain printable text is refused. */
|
|
873
|
-
signIns = new Map();
|
|
874
|
-
signInAt = new Map();
|
|
875
|
-
signedIn(vendor, as, origin) {
|
|
876
|
-
const key = `${vendor}\0${as}`;
|
|
877
|
-
if (Date.now() - (this.signInAt.get(key) ?? 0) > 43_200_000)
|
|
878
|
-
this.signIns.delete(key);
|
|
879
|
-
let held = this.signIns.get(key);
|
|
880
|
-
if (!held) {
|
|
881
|
-
this.signInAt.set(key, Date.now());
|
|
882
|
-
const twin = this.host.twinFetch(vendor);
|
|
883
|
-
held = !twin || !this.host.mirrorSignIn ? Promise.resolve(null) : this.host.mirrorSignIn(vendor, as, {
|
|
884
|
-
root: this.controlRoot(vendor),
|
|
885
|
-
twin: (path, init) => twin(new Request(`${origin}${path.startsWith('/') ? path : `/${path}`}`, init)),
|
|
886
|
-
}).then((given) => (given && /^[\x21-\x7e]{1,512}$/.test(given.token) && /^[^\x00-\x1f\x7f]{1,256}$/.test(given.account) ? given : null)).catch(() => null);
|
|
887
|
-
this.signIns.set(key, held);
|
|
888
|
-
void held.then((given) => { if (!given)
|
|
889
|
-
this.signIns.delete(key); });
|
|
890
|
-
}
|
|
891
|
-
return held;
|
|
795
|
+
/** A twin's workspaces as its discovery door lists them (`GET /twin`'s `screens`: each built workspace screen's id and
|
|
796
|
+
* path), which a person is shown at `/<served>/<vendor><path>`. Asked again after a minute, as `enforcesRead` is. */
|
|
797
|
+
workspaces = new Map();
|
|
798
|
+
async screensOf(vendor) {
|
|
799
|
+
const known = this.workspaces.get(vendor);
|
|
800
|
+
if (known !== undefined && Date.now() - known.at < 60_000)
|
|
801
|
+
return known.screens;
|
|
802
|
+
let screens = [];
|
|
803
|
+
const twin = this.host.twinFetch(vendor);
|
|
804
|
+
try {
|
|
805
|
+
const answer = twin ? await twin(new Request('http://world.local/twin', { headers: { [READ_ONLY_HEADER]: '1' } })) : null;
|
|
806
|
+
const listed = answer?.ok ? (await answer.json()).screens : undefined;
|
|
807
|
+
if (Array.isArray(listed))
|
|
808
|
+
screens = listed.filter((x) => typeof x?.id === 'string' && typeof x?.path === 'string').map(({ id, path }) => ({ id, path }));
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
screens = [];
|
|
812
|
+
}
|
|
813
|
+
this.workspaces.set(vendor, { screens, at: Date.now() });
|
|
814
|
+
return screens;
|
|
892
815
|
}
|
|
893
816
|
/** Whether a twin enforces a read-scope request itself (its manifest names `requestScopes: ['read']`):
|
|
894
817
|
* only then may the read token reach it with anything but GET or HEAD. Asked once per twin. */
|
|
@@ -951,7 +874,7 @@ export class WorldDoors {
|
|
|
951
874
|
headers.delete(CALLER_HEADER);
|
|
952
875
|
const who = this.callerOf(request);
|
|
953
876
|
if (who)
|
|
954
|
-
headers.set(CALLER_HEADER, who);
|
|
877
|
+
headers.set(CALLER_HEADER, callerForHeader(who));
|
|
955
878
|
headers.set('traceparent', traceparentForDelivery(headers.get('traceparent')) ?? newTraceparent());
|
|
956
879
|
// at a twin that enforces the read scope, every request of the read token is marked read-only (a GET
|
|
957
880
|
// that would write for its caller is refused too); at any other, its GET passes as it always has
|
|
@@ -972,19 +895,6 @@ export class WorldDoors {
|
|
|
972
895
|
// an upgrade the twin answered (a tunnel's control or visitor socket) goes back as it is
|
|
973
896
|
if (answer.status === 101)
|
|
974
897
|
return answer;
|
|
975
|
-
// THE VENDOR'S SITE: a page the twin does not serve as a page itself (it serves an OAuth screen, a redirect) is the
|
|
976
|
-
// vendor's own screens, the mirror, answered at that very address: the twin's place is the vendor's site, and a
|
|
977
|
-
// mirror's routes are real URLs, reloadable and linkable, as a dev server falls back to its app for unknown pages
|
|
978
|
-
const servedAsPage = (answer.status >= 300 && answer.status < 400) || (answer.status < 300 && (answer.headers.get('content-type') ?? '').includes('text/html'));
|
|
979
|
-
// a credential reached it: the manifest's keyless GET never becomes a page (and never signs anyone in). A resource
|
|
980
|
-
// the twin answers (a JSON link, a stored file, a PDF: 2xx) opened in a tab is that resource, as the vendor's own
|
|
981
|
-
// address would be; an address it refuses or has nothing at (a browser brings no vendor token), and the site's
|
|
982
|
-
// home, are the screens
|
|
983
|
-
const unanswered = answer.status >= 400 || tail.join('/') === '';
|
|
984
|
-
if (page && scope !== null && !servedAsPage && unanswered && (await this.host.hasMirror?.(vendor))) {
|
|
985
|
-
await answer.body?.cancel().catch(() => undefined);
|
|
986
|
-
return await this.mirror(request, url, vendor, '', true);
|
|
987
|
-
}
|
|
988
898
|
// staleness is a fact on the wire: a twin with a root says when the vendor was last observed
|
|
989
899
|
const out = new Headers(answer.headers);
|
|
990
900
|
const observedAt = this.observedAt(vendor);
|
|
@@ -998,7 +908,8 @@ export class WorldDoors {
|
|
|
998
908
|
const type = (answer.headers.get('content-type') ?? '').toLowerCase();
|
|
999
909
|
if (page && answer.status < 300 && /^(image\/svg\+xml|application\/xhtml\+xml|text\/xml|application\/xml)\b/.test(type))
|
|
1000
910
|
out.set('content-security-policy', 'sandbox');
|
|
1001
|
-
// a page and an API call to one address
|
|
911
|
+
// a page and an API call to one address may be answered differently (a stored file opened as a page is sandboxed
|
|
912
|
+
// above): caches keep them apart
|
|
1002
913
|
out.append('vary', 'Sec-Fetch-Dest');
|
|
1003
914
|
const setCookies = answer.headers.getSetCookie();
|
|
1004
915
|
if (setCookies.some((c) => /^\s*(?:__Host-)?volter_world=/i.test(c))) {
|
|
@@ -1177,7 +1088,7 @@ export class WorldDoors {
|
|
|
1177
1088
|
return Response.json({ error: 'token required' }, { status: 401 });
|
|
1178
1089
|
const parts = path.split('/').filter(Boolean);
|
|
1179
1090
|
const [kind, name, verb] = parts;
|
|
1180
|
-
// a browser session is opened by either token: the read token's is how a view-only link opens a
|
|
1091
|
+
// a browser session is opened by either token: the read token's is how a view-only link opens a vendor's screens
|
|
1181
1092
|
// the session gate comes first: a page that holds only the cookie (a sibling World's, the same site) can neither
|
|
1182
1093
|
// re-issue nor end the session through this door
|
|
1183
1094
|
const foreign = this.crossOriginSession(request, via);
|
|
@@ -1204,7 +1115,8 @@ export class WorldDoors {
|
|
|
1204
1115
|
if (Number.isNaN(Date.parse(instant)))
|
|
1205
1116
|
return Response.json({ error: 'history?at=<ISO-8601 instant>' }, { status: 400 });
|
|
1206
1117
|
const views = {};
|
|
1207
|
-
|
|
1118
|
+
const vendors = Object.keys(this.up().services);
|
|
1119
|
+
withHistoryLocks(vendors.map((vendor) => worldPaths(this.stateOf(vendor), this.controlRoot(vendor)).dir), () => { for (const vendor of vendors)
|
|
1208
1120
|
views[vendor] = historyAtInstant(this.stateOf(vendor), instant, this.controlRoot(vendor)); });
|
|
1209
1121
|
return Response.json({ world: this.served, at: new Date(Date.parse(instant)).toISOString(), views });
|
|
1210
1122
|
}
|
|
@@ -1606,7 +1518,7 @@ export class WorldDoors {
|
|
|
1606
1518
|
refresh = null;
|
|
1607
1519
|
}
|
|
1608
1520
|
const last = [...parentEntries(state, this.controlRoot(name))].reverse().find((e) => e.landsId && e.receipt);
|
|
1609
|
-
return Response.json({ twin: name, state, protocol: this.up().services[name]?.protocol ?? null,
|
|
1521
|
+
return Response.json({ twin: name, state, protocol: this.up().services[name]?.protocol ?? null, screens: await this.screensOf(name), root: root ?? null, credential: sealedCredentialInfo(this.worldRoot, name), refresh, position: wholeLog(state, this.controlRoot(name)).length, lastReceipt: last?.receipt ?? null });
|
|
1610
1522
|
}
|
|
1611
1523
|
if (verb === 'refresh' && request.method === 'POST') {
|
|
1612
1524
|
const root = rootForControlRoot(this.controlRoot(name), name);
|
|
@@ -1618,7 +1530,7 @@ export class WorldDoors {
|
|
|
1618
1530
|
return Response.json({ error: `no such door: ${request.method} /-/${this.served}/${path}` }, { status: 404 });
|
|
1619
1531
|
}
|
|
1620
1532
|
/** A browser session for this World: the presented token as an HttpOnly cookie scoped to the World's
|
|
1621
|
-
* paths, so a page served under it (a
|
|
1533
|
+
* paths, so a page served under it (a vendor's screen) reads the wire as the token's holder, in the
|
|
1622
1534
|
* token's scope. */
|
|
1623
1535
|
/** THE LOCAL SESSION: a World served on this machine's loopback never asks the person at it for a token. Any
|
|
1624
1536
|
* website can send requests to 127.0.0.1, so what stands in for the token is proof the request is this World's own
|
|
@@ -1821,7 +1733,7 @@ export class WorldDoors {
|
|
|
1821
1733
|
}
|
|
1822
1734
|
return newest;
|
|
1823
1735
|
}
|
|
1824
|
-
/** THE MAP: every twin and what it holds, by resource type, and
|
|
1736
|
+
/** THE MAP: every twin and what it holds, by resource type, and the workspaces it shows. */
|
|
1825
1737
|
async map() {
|
|
1826
1738
|
const twins = [];
|
|
1827
1739
|
for (const vendor of Object.keys(this.up().services)) {
|
|
@@ -1835,7 +1747,7 @@ export class WorldDoors {
|
|
|
1835
1747
|
counts.set(r.type, (counts.get(r.type) ?? 0) + 1);
|
|
1836
1748
|
twins.push({
|
|
1837
1749
|
twin: vendor, position: wholeLog(state, controlRoot).length,
|
|
1838
|
-
|
|
1750
|
+
screens: await this.screensOf(vendor),
|
|
1839
1751
|
root: rootForControlRoot(controlRoot, vendor) ?? null,
|
|
1840
1752
|
resources: [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([type, count]) => ({ type, count })),
|
|
1841
1753
|
});
|
|
@@ -1987,7 +1899,8 @@ export async function landChangeset(name, worldRoot, instance, changeset, expect
|
|
|
1987
1899
|
byTwin.set(item.service, [...(byTwin.get(item.service) ?? []), item.action]);
|
|
1988
1900
|
}
|
|
1989
1901
|
const landedByTwin = new Map();
|
|
1990
|
-
|
|
1902
|
+
const pushed = [...new Set([...Object.keys(expected).filter((twin) => instance.services[twin]), ...byTwin.keys()])];
|
|
1903
|
+
withHistoryLocks(pushed.map((twin) => worldPaths(stateOf(twin), join(instance.dirs.data, twin)).dir), () => {
|
|
1991
1904
|
for (const [twin, at] of Object.entries(expected)) {
|
|
1992
1905
|
if (!instance.services[twin])
|
|
1993
1906
|
throw new Error(`Unknown expected twin ${twin}`);
|
package/dist/src/tail.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// notice, never an error.
|
|
9
9
|
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
10
10
|
import { basename, dirname, join } from 'node:path';
|
|
11
|
-
import { stateDirName } from '@volter/world-core';
|
|
11
|
+
import { stateDirName } from '@volter/world-core/runtime';
|
|
12
12
|
import { statusWorld } from "./runtime.js";
|
|
13
13
|
/** Every action ledger a world service currently owns. One world service can record actions
|
|
14
14
|
* under one or more STATE services (see runtime.ts `worldServiceStateService`), so this returns
|