@volter/world-core 3.0.29 → 3.0.31
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/host-routing.cjs +106 -0
- package/dist/host-routing.d.cts +7 -0
- package/dist/inject.cjs +5 -8
- package/dist/src/actions.d.ts +3 -1
- package/dist/src/actions.js +4 -2
- package/dist/src/board-keys.d.ts +6 -0
- package/dist/src/board-keys.js +4 -0
- package/dist/src/browser-cookie-jar.d.ts +9 -0
- package/dist/src/browser-cookie-jar.js +104 -0
- package/dist/src/browser-session.d.ts +9 -0
- package/dist/src/browser-session.js +30 -0
- package/dist/src/browser-state.d.ts +52 -0
- package/dist/src/browser-state.js +105 -0
- package/dist/src/browser.d.ts +12 -0
- package/dist/src/browser.js +135 -0
- package/dist/src/derived-core.d.ts +3 -1
- package/dist/src/derived-core.js +26 -19
- package/dist/src/derived.d.ts +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/pack-fetch.js +2 -2
- package/dist/src/public-suffixes.json +1 -0
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +2 -1
- package/dist/src/serve.d.ts +3 -1
- package/dist/src/serve.js +4 -2
- package/dist/src/twin-fetch.d.ts +1 -1
- package/dist/src/twin-fetch.js +23 -4
- package/dist/vendor-hosts.cjs +13 -105
- package/dist/vendor-hosts.d.cts +3 -0
- package/host-routing.cjs +106 -0
- package/host-routing.d.cts +7 -0
- package/inject.cjs +5 -8
- package/package.json +11 -1
- package/src/actions.ts +3 -1
- package/src/board-keys.ts +4 -0
- package/src/browser-cookie-jar.ts +77 -0
- package/src/browser-session.ts +31 -0
- package/src/browser-state.ts +89 -0
- package/src/browser.ts +115 -0
- package/src/derived-core.ts +26 -16
- package/src/derived.ts +1 -1
- package/src/index.ts +4 -0
- package/src/pack-fetch.ts +2 -2
- package/src/public-suffixes.json +1 -0
- package/src/runtime.ts +4 -1
- package/src/serve.ts +3 -1
- package/src/twin-fetch.ts +9 -4
- package/vendor-hosts.cjs +13 -105
- package/vendor-hosts.d.cts +3 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { BrowserCookieJar } from "./browser-cookie-jar.js";
|
|
2
|
+
import { assertBrowserName, browserOrigin, validateBrowserState } from "./browser-state.js";
|
|
3
|
+
import { nodeBuiltin } from "./serve-http.js";
|
|
4
|
+
import { worldNow } from "./world-clock.js";
|
|
5
|
+
export { cookieMatchesHost } from "./browser-state.js";
|
|
6
|
+
/** A seed's named browser. Every state write goes through the World's single HTTP loader. */
|
|
7
|
+
export async function browser(name) {
|
|
8
|
+
assertBrowserName(name);
|
|
9
|
+
const door = process.env.VOLTER_WORLD_BROWSER_DOOR;
|
|
10
|
+
const token = process.env.VOLTER_WORLD_BROWSER_TOKEN;
|
|
11
|
+
if (!process.env.VOLTER_WORLD || !door || !token)
|
|
12
|
+
throw new Error('browser(name) runs in a World seed, with its browsers door and credential');
|
|
13
|
+
const require = nodeBuiltin('module').createRequire(import.meta.url);
|
|
14
|
+
const routing = require('../vendor-hosts.cjs');
|
|
15
|
+
const { packs } = require('../pack-facts.cjs').packFacts();
|
|
16
|
+
const resolve = async (url) => {
|
|
17
|
+
const map = routing.twinOrigins(process.env);
|
|
18
|
+
const declared = routing.resolveTwinHost(url.hostname, map, url.pathname);
|
|
19
|
+
if (declared)
|
|
20
|
+
return declared;
|
|
21
|
+
const claimed = {};
|
|
22
|
+
for (const [vendor, facts] of Object.entries(packs)) {
|
|
23
|
+
const origin = map[vendor] ?? process.env[`${routing.twinEnvStem(vendor)}_TWIN_URL`];
|
|
24
|
+
if (!origin || !facts.hostsClaimed)
|
|
25
|
+
continue;
|
|
26
|
+
map[vendor] = origin;
|
|
27
|
+
const headers = new Headers({ 'x-volter-read-only': '1' });
|
|
28
|
+
if (process.env.VOLTER_TWINS_KEY)
|
|
29
|
+
headers.set('x-twins-key', process.env.VOLTER_TWINS_KEY);
|
|
30
|
+
const listed = await fetch(`${origin}${facts.hostsClaimed.door}`, { headers })
|
|
31
|
+
.then(r => r.ok ? r.json() : null).catch(() => null);
|
|
32
|
+
claimed[vendor] = new Set(Array.isArray(listed?.hosts) ? listed.hosts.map(h => String(h).toLowerCase()) : []);
|
|
33
|
+
}
|
|
34
|
+
return routing.resolveTwinHost(url.hostname, map, url.pathname, claimed);
|
|
35
|
+
};
|
|
36
|
+
const initial = await fetch(`${door}/${name}`, { headers: { 'x-volter-token': token } });
|
|
37
|
+
if (!initial.ok && initial.status !== 404)
|
|
38
|
+
throw new Error(`browser ${name}: ${initial.status} ${await initial.text()}`);
|
|
39
|
+
const stored = initial.ok ? validateBrowserState(await initial.json()) : { cookies: [], origins: [] };
|
|
40
|
+
const jar = new BrowserCookieJar(stored.cookies);
|
|
41
|
+
const origins = structuredClone(stored.origins);
|
|
42
|
+
let queue = Promise.resolve();
|
|
43
|
+
const serial = (fn) => { const next = queue.then(fn); queue = next.then(() => undefined, () => undefined); return next; };
|
|
44
|
+
const snapshot = () => {
|
|
45
|
+
const now = new Date(worldNow()).getTime();
|
|
46
|
+
const cookies = jar.snapshot(now);
|
|
47
|
+
return validateBrowserState({ cookies, origins: structuredClone(origins) });
|
|
48
|
+
};
|
|
49
|
+
const save = async () => {
|
|
50
|
+
const response = await fetch(`${door}/${name}`, { method: 'PUT', headers: { 'content-type': 'application/json', 'x-volter-token': token }, body: JSON.stringify(snapshot()) });
|
|
51
|
+
if (!response.ok)
|
|
52
|
+
throw new Error(`browser ${name}: ${response.status} ${await response.text()}`);
|
|
53
|
+
};
|
|
54
|
+
if (initial.status === 404)
|
|
55
|
+
await save();
|
|
56
|
+
return {
|
|
57
|
+
state: () => serial(async () => snapshot()),
|
|
58
|
+
storage(origin) {
|
|
59
|
+
browserOrigin(origin);
|
|
60
|
+
const change = (key, value) => serial(async () => {
|
|
61
|
+
if (typeof key !== 'string' || (value !== undefined && typeof value !== 'string'))
|
|
62
|
+
throw new Error('browser storage: string keys and values');
|
|
63
|
+
const before = JSON.stringify(origins);
|
|
64
|
+
let entry = origins.find(o => o.origin === origin);
|
|
65
|
+
if (!entry && value !== undefined) {
|
|
66
|
+
entry = { origin, localStorage: [] };
|
|
67
|
+
origins.push(entry);
|
|
68
|
+
}
|
|
69
|
+
if (entry) {
|
|
70
|
+
entry.localStorage = entry.localStorage.filter(v => v.name !== key);
|
|
71
|
+
if (value !== undefined)
|
|
72
|
+
entry.localStorage.push({ name: key, value });
|
|
73
|
+
}
|
|
74
|
+
if (JSON.stringify(origins) !== before)
|
|
75
|
+
await save();
|
|
76
|
+
});
|
|
77
|
+
return { set: (key, value) => change(key, value), remove: key => change(key) };
|
|
78
|
+
},
|
|
79
|
+
fetch: (input, init) => serial(async () => {
|
|
80
|
+
const request = new Request(input, init);
|
|
81
|
+
let url = new URL(request.url);
|
|
82
|
+
let method = request.method;
|
|
83
|
+
let body = request.body ? await request.arrayBuffer() : undefined;
|
|
84
|
+
const headers = new Headers(request.headers);
|
|
85
|
+
if (headers.has('cookie'))
|
|
86
|
+
throw new Error('browser.fetch carries its own cookie jar');
|
|
87
|
+
for (let redirects = 0;; redirects++) {
|
|
88
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
|
|
89
|
+
throw new Error('browser.fetch takes an http or https vendor URL');
|
|
90
|
+
const target = await resolve(url);
|
|
91
|
+
if (!target || target.vendor === 'app')
|
|
92
|
+
throw new Error(`browser ${name}: no twin in this World serves ${url.hostname}${url.pathname}`);
|
|
93
|
+
const at = new URL(target.origin);
|
|
94
|
+
at.pathname = `${at.pathname.replace(/\/$/, '')}${url.pathname}`;
|
|
95
|
+
at.search = url.search;
|
|
96
|
+
const sent = new Headers(headers);
|
|
97
|
+
const cookies = jar.header(url, new Date(worldNow()).getTime());
|
|
98
|
+
if (cookies)
|
|
99
|
+
sent.set('cookie', cookies);
|
|
100
|
+
sent.set('host', new URL(target.origin).pathname.replace(/\/$/, '') ? at.host : url.host);
|
|
101
|
+
sent.set('x-volter-twin-original-host', url.host);
|
|
102
|
+
sent.set('x-forwarded-host', url.host);
|
|
103
|
+
sent.set('x-forwarded-proto', url.protocol.slice(0, -1));
|
|
104
|
+
if (process.env.VOLTER_TWINS_KEY)
|
|
105
|
+
sent.set('x-twins-key', process.env.VOLTER_TWINS_KEY);
|
|
106
|
+
const before = JSON.stringify(snapshot());
|
|
107
|
+
const response = await fetch(at, { method, headers: sent, body, redirect: 'manual', signal: request.signal });
|
|
108
|
+
for (const cookie of response.headers.getSetCookie())
|
|
109
|
+
jar.set(cookie, url, new Date(worldNow()).getTime());
|
|
110
|
+
if (JSON.stringify(snapshot()) !== before)
|
|
111
|
+
await save();
|
|
112
|
+
const location = response.headers.get('location');
|
|
113
|
+
if (!location || ![301, 302, 303, 307, 308].includes(response.status) || request.redirect === 'manual')
|
|
114
|
+
return response;
|
|
115
|
+
if (request.redirect === 'error')
|
|
116
|
+
throw new Error('browser.fetch refused a redirect');
|
|
117
|
+
if (redirects >= 20)
|
|
118
|
+
throw new Error('browser.fetch exceeded the fetch redirect limit');
|
|
119
|
+
await response.body?.cancel();
|
|
120
|
+
const next = new URL(location, url);
|
|
121
|
+
if (next.origin !== url.origin) {
|
|
122
|
+
headers.delete('authorization');
|
|
123
|
+
headers.delete('proxy-authorization');
|
|
124
|
+
}
|
|
125
|
+
if ((response.status === 303 && method !== 'HEAD') || ([301, 302].includes(response.status) && method === 'POST')) {
|
|
126
|
+
method = 'GET';
|
|
127
|
+
body = undefined;
|
|
128
|
+
for (const key of ['content-type', 'content-length', 'content-encoding', 'content-language', 'content-location'])
|
|
129
|
+
headers.delete(key);
|
|
130
|
+
}
|
|
131
|
+
url = next;
|
|
132
|
+
}
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -683,8 +683,10 @@ export declare function render(r: TwinResource): Record<string, unknown>;
|
|
|
683
683
|
* a caller that pins an instant passes its own). */
|
|
684
684
|
/** One call of a protocol-3 pack's default data (`src/semantics/seed.ts` exports `seed: SeedCall[]`): a request to the
|
|
685
685
|
* vendor's own operation (or the pack's door), made in order against the twin by the runner `volter-world init` writes
|
|
686
|
-
* beside the copied data.
|
|
686
|
+
* beside the copied data. An ordinary path is relative to the twin base. A call naming a browser uses the
|
|
687
|
+
* vendor's absolute URL and is exported separately as `browserSeed`, after the ordinary `seed` calls. */
|
|
687
688
|
export type SeedCall = {
|
|
689
|
+
browser?: string;
|
|
688
690
|
method: string;
|
|
689
691
|
path: string;
|
|
690
692
|
body?: unknown;
|
package/dist/src/derived-core.js
CHANGED
|
@@ -1166,7 +1166,6 @@ export async function contextFor(m, call, scope) {
|
|
|
1166
1166
|
body,
|
|
1167
1167
|
text,
|
|
1168
1168
|
root,
|
|
1169
|
-
occurredAt: at,
|
|
1170
1169
|
publicBase: twinPublicBase(call.request),
|
|
1171
1170
|
scenario: scenarioDecisionOf(call.request),
|
|
1172
1171
|
deliver: (url, init) => deliverToApplication(url, init),
|
|
@@ -1204,7 +1203,33 @@ export async function contextFor(m, call, scope) {
|
|
|
1204
1203
|
}
|
|
1205
1204
|
return id;
|
|
1206
1205
|
},
|
|
1206
|
+
occurredAt: at,
|
|
1207
1207
|
secret: async (label) => hmac(await worldSeed(m.service, root, at), label, 'base64url'),
|
|
1208
|
+
record: async (type, fields, id) => {
|
|
1209
|
+
if (!type.startsWith('_'))
|
|
1210
|
+
throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
|
|
1211
|
+
if (id !== undefined) {
|
|
1212
|
+
await applyTwinWrite(m.service, { operation: `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, occurredAt: at, actor: { kind: 'system' } }, root);
|
|
1213
|
+
return id;
|
|
1214
|
+
}
|
|
1215
|
+
const { value } = await applyTwinWriteAtomic(m.service, (resources) => {
|
|
1216
|
+
// Allocate and append under the same state lock: concurrent sign-ins must never share an issue id.
|
|
1217
|
+
const subject = (() => {
|
|
1218
|
+
const prefix = `${type.slice(1)}_`;
|
|
1219
|
+
let max = 0;
|
|
1220
|
+
for (const r of resources) {
|
|
1221
|
+
if (r.type !== type)
|
|
1222
|
+
continue;
|
|
1223
|
+
const n = r.id.startsWith(prefix) ? Number(r.id.slice(prefix.length)) : NaN;
|
|
1224
|
+
if (Number.isInteger(n) && n > max)
|
|
1225
|
+
max = n;
|
|
1226
|
+
}
|
|
1227
|
+
return `${prefix}${max + 1}`;
|
|
1228
|
+
})();
|
|
1229
|
+
return { kind: 'write', value: subject, write: { operation: `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, occurredAt: at, actor: { kind: 'system' } } };
|
|
1230
|
+
}, root, () => resourcesOfType(m.service, type, root));
|
|
1231
|
+
return value;
|
|
1232
|
+
},
|
|
1208
1233
|
get machines() {
|
|
1209
1234
|
const enrolled = resourcesOfType(m.service, MACHINE_POOL, root).find((r) => r.id === 'pool');
|
|
1210
1235
|
return poolFor(m.service, root, enrolled?.kind ?? 'none');
|
|
@@ -1237,24 +1262,6 @@ export async function contextFor(m, call, scope) {
|
|
|
1237
1262
|
rowsRaw: (resource, opts) => stored(m, resource, root, opts),
|
|
1238
1263
|
tree: () => twinResources(m.service, root),
|
|
1239
1264
|
history: (resource, id) => subjectHistory(m.service, { type: storedType(m, resource), id }, root).map((e) => ({ ...(e.operation ? { operation: e.operation } : {}), ...(e.fields ? { fields: e.fields } : {}), ...(e.occurredAt ? { occurredAt: e.occurredAt } : {}) })),
|
|
1240
|
-
record: async (type, fields, id) => {
|
|
1241
|
-
if (!type.startsWith('_'))
|
|
1242
|
-
throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
|
|
1243
|
-
// an id the caller does not give is the next past every one of this type the tree holds (as `{n}` ids are minted),
|
|
1244
|
-
// never a count of the rows, which a removed row would make repeat
|
|
1245
|
-
const subject = id ?? (() => {
|
|
1246
|
-
const prefix = `${type.slice(1)}_`;
|
|
1247
|
-
let max = 0;
|
|
1248
|
-
for (const r of resourcesOfType(m.service, type, root)) {
|
|
1249
|
-
const n = r.id.startsWith(prefix) ? Number(r.id.slice(prefix.length)) : NaN;
|
|
1250
|
-
if (Number.isInteger(n) && n > max)
|
|
1251
|
-
max = n;
|
|
1252
|
-
}
|
|
1253
|
-
return `${prefix}${max + 1}`;
|
|
1254
|
-
})();
|
|
1255
|
-
await applyTwinWrite(m.service, { operation: `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, occurredAt: at, actor: { kind: 'system' } }, root);
|
|
1256
|
-
return subject;
|
|
1257
|
-
},
|
|
1258
1265
|
raw: (body, init = {}) => new Response(body, { status: init.status ?? 200, headers: init.headers ?? {} }),
|
|
1259
1266
|
atomically: async (decide) => {
|
|
1260
1267
|
const { value } = await applyTwinWriteAtomic(m.service, (resources) => {
|
package/dist/src/derived.d.ts
CHANGED
|
@@ -125,6 +125,7 @@ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
|
|
|
125
125
|
export type Workspace = {
|
|
126
126
|
id: string;
|
|
127
127
|
path: string;
|
|
128
|
+
host?: string;
|
|
128
129
|
};
|
|
129
130
|
/** A page of the World's board (architecture, "The board"): a page there is to see at the vendor, a record's own (an
|
|
130
131
|
* account's profile, each of its posts, a site and each page it serves), as the vendor's pack lists them from its state
|
package/dist/src/index.d.ts
CHANGED
|
@@ -64,3 +64,5 @@ export * as redis from './redis/index.js';
|
|
|
64
64
|
export * as git from './git/index.js';
|
|
65
65
|
export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from './scenario.js';
|
|
66
66
|
export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
|
|
67
|
+
export { browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.js';
|
|
68
|
+
export { assertBrowserName, browserOrigin, validateBrowserState, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserIndexedDB } from './browser-state.js';
|
package/dist/src/index.js
CHANGED
|
@@ -59,3 +59,5 @@ export * as redis from "./redis/index.js";
|
|
|
59
59
|
export * as git from "./git/index.js";
|
|
60
60
|
// ── the scenario grammar (architecture, "Behaviour") ───────────────────────────────────────────
|
|
61
61
|
export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from "./scenario.js";
|
|
62
|
+
export { browserSessionToken, startBrowserSession, endBrowserSession } from "./browser-session.js";
|
|
63
|
+
export { assertBrowserName, browserOrigin, validateBrowserState, cookieMatchesHost } from "./browser-state.js";
|
package/dist/src/pack-fetch.js
CHANGED
|
@@ -19,7 +19,7 @@ import { deferPerform, performRequestGroup, RefusedWriteError, VendorWriteError
|
|
|
19
19
|
import { withCors } from "./cors.js";
|
|
20
20
|
import { contentAddressOf } from "./content-route.js";
|
|
21
21
|
import { getPack } from "./packRegistry.js";
|
|
22
|
-
import { hostRules } from '../
|
|
22
|
+
import { hostRules } from '../host-routing.cjs';
|
|
23
23
|
import { socketUpgrade } from "./sockets.js";
|
|
24
24
|
import { MACHINE_POOL, POOL_KINDS } from "./machines.js";
|
|
25
25
|
import { applyTwinWrite } from "./serve.js";
|
|
@@ -220,7 +220,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
220
220
|
});
|
|
221
221
|
// the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path, the pack's own
|
|
222
222
|
// and its lanes'
|
|
223
|
-
const workspaces = () => [...(m.screens ?? []).filter((s) => s.kind === 'workspace' && s.status === 'done' && parts.screens?.[s.id]).map((s) => ({ id: s.id, path: s.path })), ...lanesWorkspaces(parts.lanes ?? {})];
|
|
223
|
+
const workspaces = () => [...(m.screens ?? []).filter((s) => s.kind === 'workspace' && s.status === 'done' && parts.screens?.[s.id]).map((s) => ({ id: s.id, path: s.path, host: s.host })), ...lanesWorkspaces(parts.lanes ?? {})];
|
|
224
224
|
/** Whether a built screen a person views (a workspace or a flow, never content an API client reads) draws a url: its host
|
|
225
225
|
* one the screen names, its path the screen's or under it (a root screen draws its root alone). */
|
|
226
226
|
// by what the pack declares built (a screen drawn by another's handler, as X's profile is by its web pages, is built)
|