@volter/world-core 3.0.163 → 3.0.165
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/generated/pack-facts.json +44 -3660
- package/dist/pack-facts.cjs +4 -4
- package/dist/src/browser-session.d.ts +7 -1
- package/dist/src/browser-session.js +21 -10
- package/dist/src/browser.d.ts +4 -0
- package/dist/src/browser.js +10 -0
- package/dist/src/derived-core.d.ts +46 -6
- package/dist/src/derived-core.js +60 -13
- package/dist/src/derived-real.js +159 -5
- package/dist/src/derived.d.ts +1 -1
- package/dist/src/events.d.ts +6 -3
- package/dist/src/events.js +52 -7
- package/dist/src/executor.d.ts +2 -2
- package/dist/src/executor.js +13 -0
- package/dist/src/file-response.d.ts +6 -2
- package/dist/src/file-response.js +29 -5
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/smart-http-client.d.ts +28 -0
- package/dist/src/git/smart-http-client.js +95 -0
- package/dist/src/git/smart-http.d.ts +3 -0
- package/dist/src/git/smart-http.js +9 -0
- package/dist/src/head.d.ts +16 -1
- package/dist/src/head.js +52 -7
- package/dist/src/html.d.ts +29 -0
- package/dist/src/html.js +64 -0
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +5 -1
- package/dist/src/managed-database.d.ts +4 -2
- package/dist/src/managed-database.js +1 -0
- package/dist/src/media-header.d.ts +34 -0
- package/dist/src/media-header.js +129 -0
- package/dist/src/pack-fetch.d.ts +24 -3
- package/dist/src/pack-fetch.js +155 -96
- package/dist/src/pack-paths.d.ts +2 -3
- package/dist/src/pack-paths.js +3 -13
- package/dist/src/packRegistry.d.ts +17 -1
- package/dist/src/request-body.d.ts +2 -3
- package/dist/src/request-body.js +2 -3
- package/dist/src/runtime.d.ts +4 -1
- package/dist/src/runtime.js +5 -1
- package/dist/src/serve-http.d.ts +2 -2
- package/dist/src/serve-http.js +104 -19
- package/dist/src/smtp-server.d.ts +78 -0
- package/dist/src/smtp-server.js +160 -0
- package/dist/src/smtp.d.ts +29 -11
- package/dist/src/smtp.js +221 -52
- package/generated/pack-facts.json +44 -3660
- package/pack-facts.cjs +4 -4
- package/package.json +4 -2
- package/src/browser-session.ts +17 -5
- package/src/browser.ts +10 -0
- package/src/derived-core.ts +77 -19
- package/src/derived-real.ts +131 -4
- package/src/derived.ts +2 -2
- package/src/events.ts +54 -9
- package/src/executor.ts +13 -2
- package/src/file-response.ts +28 -6
- package/src/git/index.ts +1 -0
- package/src/git/smart-http-client.ts +99 -0
- package/src/git/smart-http.ts +7 -0
- package/src/head.ts +37 -6
- package/src/html.ts +72 -0
- package/src/index.ts +6 -2
- package/src/managed-database.ts +4 -3
- package/src/media-header.ts +108 -0
- package/src/pack-fetch.ts +142 -87
- package/src/pack-paths.ts +3 -11
- package/src/packRegistry.ts +9 -1
- package/src/request-body.ts +2 -3
- package/src/runtime.ts +5 -1
- package/src/serve-http.ts +81 -19
- package/src/smtp-server.ts +166 -0
- package/src/smtp.ts +199 -53
package/src/pack-fetch.ts
CHANGED
|
@@ -19,7 +19,9 @@ import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from './
|
|
|
19
19
|
import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest, type PackScenarioAdapter } from './scenario.ts';
|
|
20
20
|
import { getActiveWorldStore } from './world-store.ts';
|
|
21
21
|
import { runWithCorrelationId } from './actions.ts';
|
|
22
|
-
import { deferPerform, performRequestGroup, RefusedWriteError, VendorWriteError } from './head.ts';
|
|
22
|
+
import { deferPerform, deployableEntries, performRequestGroup, RefusedWriteError, VendorWriteError } from './head.ts';
|
|
23
|
+
import { FLUSH, ZERO_SHA, pktLine, readPktLines } from './git/smart-http.ts';
|
|
24
|
+
import { concat } from './git/objects.ts';
|
|
23
25
|
import { withCors } from './cors.ts';
|
|
24
26
|
import type { ApplicationStandIn } from './events.ts';
|
|
25
27
|
import { contentAddressOf } from './content-route.ts';
|
|
@@ -32,7 +34,7 @@ import { expireSocketWaits, socketUpgrade, type SocketEngine } from './sockets.t
|
|
|
32
34
|
import { liveIngest } from './live/ingest.ts';
|
|
33
35
|
import type { LiveEngine } from './live/types.ts';
|
|
34
36
|
import { readResourceBlob, removeResourceBlob, writeResourceBlob } from './resource-blob.ts';
|
|
35
|
-
import { resourcesOfType } from './derived-core.ts';
|
|
37
|
+
import { gitRepositoryOf, resourcesOfType } from './derived-core.ts';
|
|
36
38
|
import { worldNow } from './world-clock.ts';
|
|
37
39
|
import type { WebSocketUpgrade, WebSocketPeer } from './serve-http.ts';
|
|
38
40
|
import { MACHINE_POOL, POOL_KINDS, resetFakePool } from './machines.ts';
|
|
@@ -105,7 +107,7 @@ export type LanePart = {
|
|
|
105
107
|
* World's scenario file for a model vendor's turns (`scenarioPath`, the runtime's `--scenario`). */
|
|
106
108
|
export type PackFetchOptions = {
|
|
107
109
|
root?: string; readOnly?: boolean; clock?: () => string; database?: string; scenarioPath?: string;
|
|
108
|
-
/** the application a walk stands in for
|
|
110
|
+
/** the application a walk stands in for: this fetch's requests and declared events are answered by it */
|
|
109
111
|
application?: ApplicationStandIn;
|
|
110
112
|
};
|
|
111
113
|
|
|
@@ -143,7 +145,7 @@ function hostMatcher(pattern: string): (host: string) => Match | undefined {
|
|
|
143
145
|
};
|
|
144
146
|
}
|
|
145
147
|
/** The host a request is for: the vendor host the World's injector names, else its own. */
|
|
146
|
-
const hostOf = (request: Request): string => readHostPort(request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host)?.host ?? '';
|
|
148
|
+
export const hostOf = (request: Request): string => readHostPort(request.headers.get('x-volter-twin-original-host') ?? request.headers.get('host') ?? new URL(request.url).host)?.host ?? '';
|
|
147
149
|
|
|
148
150
|
/** Where a workspace asked `?show=<type>:<id>` opens the record (architecture, "Workspace"): the built workspace page
|
|
149
151
|
* that shows that type, its parameter the id; else the one that lists the type; else the workspace's home (its root
|
|
@@ -218,13 +220,26 @@ async function encoded(m: DerivedManifest, request: Request, res: Response): Pro
|
|
|
218
220
|
return new Response(buffered, { status: res.status, statusText: res.statusText, headers });
|
|
219
221
|
}
|
|
220
222
|
|
|
221
|
-
/** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
|
|
222
223
|
/** A door's path as it is served and listed: under `/_twin/`, whether the manifest writes the prefix or not. */
|
|
223
224
|
const doorPath = (path: string): string => (path.startsWith('/_twin/') ? path : `/_twin${path.startsWith('/') ? '' : '/'}${path}`);
|
|
224
225
|
|
|
226
|
+
/** A declared World door, matched by the same normalized path for serving and response ownership. */
|
|
227
|
+
export function worldDoorMatcher<Door extends { method: string; path: string }>(doors: ReadonlyArray<Door>): (method: string, path: string) => { door: Door; params: Record<string, string> } | undefined {
|
|
228
|
+
const routes = doors.map((door) => ({ door, method: door.method.toUpperCase(), matches: pathMatcher(doorPath(door.path), false) }));
|
|
229
|
+
return (method, path) => {
|
|
230
|
+
const at = path.replace(/\/+$/, '') || '/';
|
|
231
|
+
for (const route of routes) {
|
|
232
|
+
if (route.method !== method.toUpperCase()) continue;
|
|
233
|
+
const hit = route.matches(at);
|
|
234
|
+
if (hit) return { door: route.door, params: hit.params };
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
225
240
|
/** A request with its path's repeated slashes merged, as a front that merges them routes it (the manifest's
|
|
226
241
|
* `mergeSlashes`); one with none is itself. */
|
|
227
|
-
function slashesMerged(request: Request): Request {
|
|
242
|
+
export function slashesMerged(request: Request): Request {
|
|
228
243
|
const url = new URL(request.url);
|
|
229
244
|
const merged = url.pathname.replace(/\/{2,}/g, '/');
|
|
230
245
|
if (merged === url.pathname) return request;
|
|
@@ -232,6 +247,58 @@ function slashesMerged(request: Request): Request {
|
|
|
232
247
|
return new Request(url, request);
|
|
233
248
|
}
|
|
234
249
|
|
|
250
|
+
/** The selected unit's request transformation, shared by serving and response judgment. */
|
|
251
|
+
export function unitRequestRouter(m: Pick<DerivedManifest, 'pathPrefix' | 'basePathAlias' | 'pathFromHost' | 'hostParams' | 'headerParams'>, surface: DerivedSurface, routes: ReturnType<typeof compileSurface>): (request: Request) => Request {
|
|
252
|
+
const prefix = m.pathPrefix ? new RegExp(`^(?:${m.pathPrefix})(?=/)`) : undefined;
|
|
253
|
+
const base = surface.basePath ?? '';
|
|
254
|
+
const baseAlias = m.basePathAlias && base ? new RegExp(`^(?:${m.basePathAlias})(?=/)`) : undefined;
|
|
255
|
+
const hostPath = m.pathFromHost ? new RegExp(m.pathFromHost, 'i') : undefined;
|
|
256
|
+
const hostParams = m.hostParams ? new RegExp(m.hostParams, 'i') : undefined;
|
|
257
|
+
// each operation's spanning parameters, keyed by the operation and the parameter as the route compiles them (a whole
|
|
258
|
+
// segment's label whose name the surface or the manifest declares spanning), so one operation's spanning name never
|
|
259
|
+
// keeps another's slashes, nor a label that only part of a segment holds
|
|
260
|
+
const spanningNames = new Set(surface.spanning ?? []);
|
|
261
|
+
const spanningOf = new Set(surface.operations.flatMap((op) => ((op.basePath ?? base) + op.path).split('/').flatMap((seg) => {
|
|
262
|
+
const label = /^\{([^}]+)\}$/.exec(seg)?.[1];
|
|
263
|
+
return label !== undefined && spanningNames.has(label) ? [`${op.id}\u0000${label}`] : [];
|
|
264
|
+
})));
|
|
265
|
+
/** A path as the vendor routes it: the manifest's path prefix taken off, the spec's base in an alias's place, a host's
|
|
266
|
+
* path label (pathFromHost) put in front. */
|
|
267
|
+
const rewritten = (pathname: string, host: string): string => {
|
|
268
|
+
let path = pathname;
|
|
269
|
+
if (prefix) path = path.replace(prefix, '') || '/';
|
|
270
|
+
// a base the vendor serves as its own: the spec's base in its place (a path already under the spec's base stays)
|
|
271
|
+
if (baseAlias && !(path === base || path.startsWith(`${base}/`))) path = path.replace(baseAlias, base);
|
|
272
|
+
const label = hostPath?.exec(host)?.[1];
|
|
273
|
+
if (label) path = `/${label}${path === '/' ? '' : path}`;
|
|
274
|
+
return path;
|
|
275
|
+
};
|
|
276
|
+
/** The request as its vendor routes it: duplicate slashes collapsed, unless the rewritten path matches an operation
|
|
277
|
+
* through one of that operation's spanning parameters (the surface's and the manifest's), whose value keeps its
|
|
278
|
+
* slashes; then rewritten. */
|
|
279
|
+
return (request: Request): Request => {
|
|
280
|
+
const url = new URL(request.url);
|
|
281
|
+
const host = hostOf(request);
|
|
282
|
+
const kept = rewritten(url.pathname, host);
|
|
283
|
+
const matched = matchOperation(routes, request.method, kept);
|
|
284
|
+
const spanning = matched !== undefined && Object.keys(matched.params).some((name) => spanningOf.has(`${matched.operation.id}\u0000${name}`));
|
|
285
|
+
const source = spanning ? url.pathname : url.pathname.replace(/\/{2,}/g, '/');
|
|
286
|
+
// a prefix's named groups (Jira's `/ex/jira/(?<cloudId>[^/]+)`) and the host's (E2B's envd at
|
|
287
|
+
// `49983-(?<sandboxID>…).e2b.app`) join the operation's parameters
|
|
288
|
+
const groups = { ...(prefix?.exec(source)?.groups ?? {}), ...(hostParams?.exec(host)?.groups ?? {}), ...Object.fromEntries(Object.entries(m.headerParams ?? {}).flatMap(([param, header]) => { const value = request.headers.get(header); return value === null ? [] : [[param, value]]; })) };
|
|
289
|
+
const path = spanning ? kept : rewritten(source, host);
|
|
290
|
+
if (path === url.pathname && !Object.keys(groups).length) return request;
|
|
291
|
+
// the path the client sent, which a signature over the request (SigV4's canonical URI) covers
|
|
292
|
+
const headers = new Headers(request.headers);
|
|
293
|
+
// the path as first sent: a front before this (an attach bridge) may have named the one its client signed
|
|
294
|
+
if (!headers.has(ORIGINAL_PATH_HEADER)) headers.set(ORIGINAL_PATH_HEADER, url.pathname);
|
|
295
|
+
if (Object.keys(groups).length) headers.set(PREFIX_PARAMS_HEADER, JSON.stringify(groups));
|
|
296
|
+
url.pathname = path;
|
|
297
|
+
return new Request(url, { method: request.method, headers, body: request.body, signal: request.signal, ...(request.body ? { duplex: 'half' } : {}) } as RequestInit);
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
|
|
235
302
|
export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}): DerivedFetch {
|
|
236
303
|
const m = parts.manifest;
|
|
237
304
|
const scope: CoreScope = {
|
|
@@ -322,11 +389,8 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
322
389
|
if (decision.kind === 'fault') return Response.json(decision.result.body, { status: decision.result.status, headers: decision.result.headers ?? {} });
|
|
323
390
|
return api(withHeaders(request, new Headers([...request.headers, [SCENARIO_DECISION_HEADER, scenarioDecisionHeader(decision)]])));
|
|
324
391
|
};
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
if (!answer) throw new Error(`${m.vendor}: the manifest declares the door ${d.id}, and semantics/doors.ts exports no ${d.id}`);
|
|
328
|
-
return { decl: d, path: pathMatcher(doorPath(d.path), false), answer };
|
|
329
|
-
});
|
|
392
|
+
for (const d of m.doors ?? []) if (!parts.doors?.[d.id]) throw new Error(`${m.vendor}: the manifest declares the door ${d.id}, and semantics/doors.ts exports no ${d.id}`);
|
|
393
|
+
const door = worldDoorMatcher(m.doors ?? []);
|
|
330
394
|
const screens = (m.screens ?? []).filter((s) => s.status === 'done' && parts.screens?.[s.id]).map((s) => {
|
|
331
395
|
const hosts = [s.host, ...(s.hosts ?? [])].map(hostMatcher);
|
|
332
396
|
return { decl: s, host: (h: string) => hosts.map((match) => match(h)).find(Boolean), path: pathMatcher(s.path, true), answer: parts.screens![s.id]! };
|
|
@@ -355,53 +419,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
355
419
|
};
|
|
356
420
|
const refusedReadOnly = (): Response => vendorError(m, m.readOnly);
|
|
357
421
|
|
|
358
|
-
const
|
|
359
|
-
const base = surface.basePath ?? '';
|
|
360
|
-
const baseAlias = m.basePathAlias && base ? new RegExp(`^(?:${m.basePathAlias})(?=/)`) : undefined;
|
|
361
|
-
const hostPath = m.pathFromHost ? new RegExp(m.pathFromHost, 'i') : undefined;
|
|
362
|
-
const hostParams = m.hostParams ? new RegExp(m.hostParams, 'i') : undefined;
|
|
363
|
-
// each operation's spanning parameters, keyed by the operation and the parameter as the route compiles them (a whole
|
|
364
|
-
// segment's label whose name the surface or the manifest declares spanning), so one operation's spanning name never
|
|
365
|
-
// keeps another's slashes, nor a label that only part of a segment holds
|
|
366
|
-
const spanningNames = new Set(surface.spanning ?? []);
|
|
367
|
-
const spanningOf = new Set(surface.operations.flatMap((op) => ((op.basePath ?? base) + op.path).split('/').flatMap((seg) => {
|
|
368
|
-
const label = /^\{([^}]+)\}$/.exec(seg)?.[1];
|
|
369
|
-
return label !== undefined && spanningNames.has(label) ? [`${op.id}\u0000${label}`] : [];
|
|
370
|
-
})));
|
|
371
|
-
/** A path as the vendor routes it: the manifest's path prefix taken off, the spec's base in an alias's place, a host's
|
|
372
|
-
* path label (pathFromHost) put in front. */
|
|
373
|
-
const rewritten = (pathname: string, host: string): string => {
|
|
374
|
-
let path = pathname;
|
|
375
|
-
if (prefix) path = path.replace(prefix, '') || '/';
|
|
376
|
-
// a base the vendor serves as its own: the spec's base in its place (a path already under the spec's base stays)
|
|
377
|
-
if (baseAlias && !(path === base || path.startsWith(`${base}/`))) path = path.replace(baseAlias, base);
|
|
378
|
-
const label = hostPath?.exec(host)?.[1];
|
|
379
|
-
if (label) path = `/${label}${path === '/' ? '' : path}`;
|
|
380
|
-
return path;
|
|
381
|
-
};
|
|
382
|
-
/** The request as its vendor routes it: duplicate slashes collapsed, unless the rewritten path matches an operation
|
|
383
|
-
* through one of that operation's spanning parameters (the surface's and the manifest's), whose value keeps its
|
|
384
|
-
* slashes; then rewritten. */
|
|
385
|
-
const routed = (request: Request): Request => {
|
|
386
|
-
const url = new URL(request.url);
|
|
387
|
-
const host = hostOf(request);
|
|
388
|
-
const kept = rewritten(url.pathname, host);
|
|
389
|
-
const matched = matchOperation(routes, request.method, kept);
|
|
390
|
-
const spanning = matched !== undefined && Object.keys(matched.params).some((name) => spanningOf.has(`${matched.operation.id}\u0000${name}`));
|
|
391
|
-
const source = spanning ? url.pathname : url.pathname.replace(/\/{2,}/g, '/');
|
|
392
|
-
// a prefix's named groups (Jira's `/ex/jira/(?<cloudId>[^/]+)`) and the host's (E2B's envd at
|
|
393
|
-
// `49983-(?<sandboxID>…).e2b.app`) join the operation's parameters
|
|
394
|
-
const groups = { ...(prefix?.exec(source)?.groups ?? {}), ...(hostParams?.exec(host)?.groups ?? {}), ...Object.fromEntries(Object.entries(m.headerParams ?? {}).flatMap(([param, header]) => { const value = request.headers.get(header); return value === null ? [] : [[param, value]]; })) };
|
|
395
|
-
const path = spanning ? kept : rewritten(source, host);
|
|
396
|
-
if (path === url.pathname && !Object.keys(groups).length) return request;
|
|
397
|
-
// the path the client sent, which a signature over the request (SigV4's canonical URI) covers
|
|
398
|
-
const headers = new Headers(request.headers);
|
|
399
|
-
// the path as first sent: a front before this (an attach bridge) may have named the one its client signed
|
|
400
|
-
if (!headers.has(ORIGINAL_PATH_HEADER)) headers.set(ORIGINAL_PATH_HEADER, url.pathname);
|
|
401
|
-
if (Object.keys(groups).length) headers.set(PREFIX_PARAMS_HEADER, JSON.stringify(groups));
|
|
402
|
-
url.pathname = path;
|
|
403
|
-
return new Request(url, { method: request.method, headers, body: request.body, signal: request.signal, ...(request.body ? { duplex: 'half' } : {}) } as RequestInit);
|
|
404
|
-
};
|
|
422
|
+
const routed = unitRequestRouter(m, surface, routes);
|
|
405
423
|
const rootServes = (method: string, path: string): boolean => Boolean(matchOperation(routes, method, path));
|
|
406
424
|
const lane = m.lanes ? laneRouter(m.vendor, m.lanes, parts.lanes ?? {}, rootServes) : undefined;
|
|
407
425
|
// Inspect lane-owned World doors directly, without routing them back through the vendor gateway.
|
|
@@ -488,7 +506,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
488
506
|
const own = await answerContent(arrived);
|
|
489
507
|
if (own) return own;
|
|
490
508
|
// Root World doors belong to the pack at every routed host, including a lane's host.
|
|
491
|
-
const rootDoor =
|
|
509
|
+
const rootDoor = door(arrived.method, asked);
|
|
492
510
|
const toLane = discovery || rootDoor ? undefined : lane?.(arrived);
|
|
493
511
|
if (toLane) return toLane(arrived);
|
|
494
512
|
const readable = await prepared(arrived);
|
|
@@ -551,12 +569,11 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
551
569
|
if (kind === 'fake') resetFakePool(m.service, options.root);
|
|
552
570
|
return Response.json({ kind }, { status: 201 });
|
|
553
571
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const hit = d.path(path);
|
|
557
|
-
if (!hit) continue;
|
|
572
|
+
const hit = door(request.method, path);
|
|
573
|
+
if (hit) {
|
|
558
574
|
if (readOnly && request.method !== 'GET') return refusedReadOnly();
|
|
559
|
-
|
|
575
|
+
const d = hit.door;
|
|
576
|
+
return parts.doors![d.id]!(await open(request, d.id, d.method, d.path, hit.params));
|
|
560
577
|
}
|
|
561
578
|
return gapAnswer(m, request);
|
|
562
579
|
}
|
|
@@ -628,7 +645,38 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
628
645
|
const id = `req_${globalThis.crypto.randomUUID()}`;
|
|
629
646
|
const { value: answer, appended } = await runWithCorrelationId(id, () => deferPerform(id, () => fetch(request)));
|
|
630
647
|
if (!appended) return answer;
|
|
648
|
+
// a receive-pack's pushes (derived-core.ts pushedInput): what the World's refs held before and after each
|
|
649
|
+
const pushes = m.git ? deployableEntries(m.service, options.root).filter((e) => e.correlationId === id && e.input?.git).map((e) => ({ actionId: e.id, ...(e.input!.git as { name: string; ref: string; before: string; after: string }) })) : [];
|
|
631
650
|
const performed = await runWithCorrelationId(id, () => performRequestGroup(m.service, id, options.root));
|
|
651
|
+
// under `auto`, what the root did is what the World keeps (ADR 0031): a ref it refused goes back to its value before
|
|
652
|
+
// the push, by compare-and-set under the ref lock (a later push built on it keeps its value), its push and effects
|
|
653
|
+
// are reverted (head.ts), and the client hears git's own report, `ok` or `ng` for each ref
|
|
654
|
+
if (pushes.length) {
|
|
655
|
+
const whole = performed instanceof RefusedWriteError ? performed.reason : performed instanceof VendorWriteError ? performed.message : undefined;
|
|
656
|
+
const refused = new Map<string, string>();
|
|
657
|
+
for (const p of pushes) {
|
|
658
|
+
const reason = whole ?? (performed as { refused?: Array<{ actionId: string; reason: string }> }).refused?.find((r) => r.actionId === p.actionId)?.reason;
|
|
659
|
+
if (reason !== undefined) refused.set(p.actionId, reason);
|
|
660
|
+
}
|
|
661
|
+
if (refused.size) {
|
|
662
|
+
for (const p of [...pushes].reverse()) {
|
|
663
|
+
if (!refused.has(p.actionId)) continue;
|
|
664
|
+
const { refs } = gitRepositoryOf(m, p.name, options.root);
|
|
665
|
+
refs.withLock(() => {
|
|
666
|
+
if ((refs.get(p.ref) ?? ZERO_SHA) !== p.after) return;
|
|
667
|
+
if (p.before === ZERO_SHA) refs.delete(p.ref); else refs.set(p.ref, p.before);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
const ng = new Map(pushes.filter((p) => refused.has(p.actionId)).map((p) => [p.ref, refused.get(p.actionId)!.replace(/\s+/g, ' ')]));
|
|
671
|
+
const report = concat(...readPktLines(new Uint8Array(await answer.arrayBuffer())).lines.map((line) => {
|
|
672
|
+
const ref = line.startsWith('ok ') ? line.slice(3).trim() : undefined;
|
|
673
|
+
return pktLine(ref !== undefined && ng.has(ref) ? `ng ${ref} ${ng.get(ref)}\n` : line);
|
|
674
|
+
}), FLUSH);
|
|
675
|
+
const headers = new Headers(answer.headers);
|
|
676
|
+
headers.delete('content-length');
|
|
677
|
+
return new Response(report as unknown as BodyInit, { status: answer.status, headers });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
632
680
|
// the vendor's refusal, as its status and words (perform names them `<status>: <message>`), in the vendor's error body
|
|
633
681
|
if (performed instanceof RefusedWriteError) { const said = /^(\d{3}): ([\s\S]*)$/.exec(performed.reason); return vendorError(m, { status: said ? Number(said[1]) : 400, message: said ? said[2]! : performed.reason }); }
|
|
634
682
|
if (performed instanceof VendorWriteError) return vendorError(m, { status: 502, message: performed.message });
|
|
@@ -717,30 +765,16 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
717
765
|
return derivedRequestScopes(m, Object.assign(served, { owners, ownersByUnit, serves, advanceClock: async (request: Request) => runWithStorageTenant(m.service, await selectedAccount(request), () => advanceClock(request)), workspaces, boards: Boolean(parts.board), maxRequestBodySize: m.descriptor?.maxRequestBodySize, draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}), ...(ingest ? { ingest } : {}) }), options.readOnly);
|
|
718
766
|
}
|
|
719
767
|
|
|
720
|
-
/** The
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const served = (name: string): ((request: Request) => Promise<Response>) => {
|
|
724
|
-
const part = lanes[name];
|
|
725
|
-
if (!part) throw new Error(`${vendor}: the manifest routes to the lane ${name}, and the vendor's fetch is given no ${name}`);
|
|
726
|
-
// the lane is named on the request, so a write it makes records which lane's API made it (LANE_HEADER)
|
|
727
|
-
const named = (request: Request): Promise<Response> => {
|
|
728
|
-
const headers = new Headers(request.headers);
|
|
729
|
-
headers.set(LANE_HEADER, name);
|
|
730
|
-
return part.fetch(withHeaders(request, headers));
|
|
731
|
-
};
|
|
732
|
-
return decl.cors ? withCors(decl.cors, named) : named;
|
|
733
|
-
};
|
|
734
|
-
const doors = Object.entries(lanes).flatMap(([name, part]) => (part.manifest.doors ?? []).map((d) => ({
|
|
735
|
-
name, method: d.method.toUpperCase(), matches: pathMatcher(d.path.startsWith('/_twin/') ? d.path : `/_twin/${d.path.replace(/^\//, '')}`, false),
|
|
736
|
-
})));
|
|
768
|
+
/** The lane selected by the vendor gateway, shared by serving and response judgment. */
|
|
769
|
+
export function laneSelector(decl: LanesDecl, lanes: Record<string, { manifest: LanePart['manifest']; fetch: Pick<LanePart['fetch'], 'serves' | 'contentHosts'> }>, rootServes: (method: string, path: string) => boolean = () => false): (request: Request) => string | undefined {
|
|
770
|
+
const door = worldDoorMatcher(Object.entries(lanes).flatMap(([name, part]) => (part.manifest.doors ?? []).map((d) => ({ ...d, name }))));
|
|
737
771
|
const laneRoutes = decl.routes.map((r) => {
|
|
738
772
|
const at = r.path?.replace(/\/+$/, '');
|
|
739
773
|
const host = r.host !== undefined ? new RegExp(r.host, 'i') : undefined;
|
|
740
774
|
const except = (r.exceptHosts ?? []).map((h) => h.toLowerCase());
|
|
741
775
|
const own = lanes[r.lane]?.fetch.serves;
|
|
742
776
|
return {
|
|
743
|
-
lane:
|
|
777
|
+
lane: r.lane,
|
|
744
778
|
holds: (request: Request, path: string, name: string): boolean =>
|
|
745
779
|
(at === undefined || path === at || path.startsWith(`${at}/`))
|
|
746
780
|
&& (host === undefined || host.test(name))
|
|
@@ -749,21 +783,42 @@ function laneRouter(vendor: string, decl: LanesDecl, lanes: Record<string, LaneP
|
|
|
749
783
|
&& !(r.unlessOnlyRoot && rootServes(request.method, path) && !(own?.(request.method, path) ?? false)),
|
|
750
784
|
};
|
|
751
785
|
});
|
|
752
|
-
const fallback = decl.default !== undefined ? served(decl.default) : undefined;
|
|
753
786
|
return (request) => {
|
|
754
787
|
// `<base>/@<host>/<path>` at a World's place, for a host a lane's built content screen names, goes to that lane, before
|
|
755
788
|
// any lane is picked by the request's host or path: its own fetch answers it by its content route, under the vendor's
|
|
756
789
|
// host rules (architecture, "A page at a World's place")
|
|
757
790
|
const address = request.headers.has(TWIN_PREFIX_HEADER) ? contentAddressOf(new URL(request.url).pathname) : undefined;
|
|
758
791
|
const content = address && Object.keys(lanes).find((l) => lanes[l]!.fetch.contentHosts?.includes(address.host));
|
|
759
|
-
if (content) return
|
|
792
|
+
if (content) return content;
|
|
760
793
|
const path = new URL(request.url).pathname.replace(/\/{2,}/g, '/');
|
|
761
794
|
if (path.startsWith('/_twin/')) {
|
|
762
|
-
const
|
|
763
|
-
if (
|
|
795
|
+
const hit = door(request.method, path);
|
|
796
|
+
if (hit) return hit.door.name;
|
|
764
797
|
}
|
|
765
798
|
const name = hostOf(request);
|
|
766
|
-
return laneRoutes.find((r) => r.holds(request, path, name))?.lane ??
|
|
799
|
+
return laneRoutes.find((r) => r.holds(request, path, name))?.lane ?? decl.default;
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** The vendor's gateway over its lanes: the lane a request goes to (a door to the lane that declares it, then the first
|
|
804
|
+
* route whose conditions all hold, then the default), behind the gateway's CORS; undefined when none takes it. */
|
|
805
|
+
function laneRouter(vendor: string, decl: LanesDecl, lanes: Record<string, LanePart>, rootServes: (method: string, path: string) => boolean = () => false): (request: Request) => ((request: Request) => Promise<Response>) | undefined {
|
|
806
|
+
const served = (name: string): ((request: Request) => Promise<Response>) => {
|
|
807
|
+
const part = lanes[name];
|
|
808
|
+
if (!part) throw new Error(`${vendor}: the manifest routes to the lane ${name}, and the vendor's fetch is given no ${name}`);
|
|
809
|
+
// the lane is named on the request, so a write it makes records which lane's API made it (LANE_HEADER)
|
|
810
|
+
const named = (request: Request): Promise<Response> => {
|
|
811
|
+
const headers = new Headers(request.headers);
|
|
812
|
+
headers.set(LANE_HEADER, name);
|
|
813
|
+
return part.fetch(withHeaders(request, headers));
|
|
814
|
+
};
|
|
815
|
+
return decl.cors ? withCors(decl.cors, named) : named;
|
|
816
|
+
};
|
|
817
|
+
const selected = laneSelector(decl, lanes, rootServes);
|
|
818
|
+
const handlers = new Map([...new Set([...decl.routes.map((route) => route.lane), ...(decl.default === undefined ? [] : [decl.default]), ...Object.keys(lanes)])].map((name) => [name, served(name)]));
|
|
819
|
+
return (request) => {
|
|
820
|
+
const name = selected(request);
|
|
821
|
+
return name === undefined ? undefined : handlers.get(name);
|
|
767
822
|
};
|
|
768
823
|
}
|
|
769
824
|
|
package/src/pack-paths.ts
CHANGED
|
@@ -102,20 +102,12 @@ export function installedPackageDir(from: string, name: string): string | undefi
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/**
|
|
106
|
-
*
|
|
107
|
-
* sibling checkout merely because one happens to be beside them. */
|
|
105
|
+
/** The pack repository a World is rooted in, when it is one (ADR 0034: a platform checkout finds no pack repository
|
|
106
|
+
* beside it; a pack reaches any other World as an installed or linked package). */
|
|
108
107
|
export function packRepository(from: string): string | undefined {
|
|
109
108
|
let dir = resolve(from);
|
|
110
109
|
for (;;) {
|
|
111
110
|
if (packageName(dir) === 'twin-packs-p3') return dir;
|
|
112
|
-
if (packageName(join(dir, 'packages', 'world-core')) === '@volter/world-core') {
|
|
113
|
-
const env = globalThis.process?.env?.VOLTER_PACKS;
|
|
114
|
-
const named = env ? resolve(env) : undefined;
|
|
115
|
-
if (named && packageName(named) === 'twin-packs-p3') return named;
|
|
116
|
-
const sibling = join(dirname(dir), 'twin-packs-p3');
|
|
117
|
-
if (packageName(sibling) === 'twin-packs-p3') return sibling;
|
|
118
|
-
}
|
|
119
111
|
const up = dirname(dir);
|
|
120
112
|
if (up === dir) return undefined;
|
|
121
113
|
dir = up;
|
|
@@ -125,7 +117,7 @@ export function packRepository(from: string): string | undefined {
|
|
|
125
117
|
/** Resolve one vendor's package directory for host-side reads (assets, infrastructure launchers). */
|
|
126
118
|
export function vendorPackageDir(from: string, vendor: string): string | undefined {
|
|
127
119
|
if (!/^[a-z][a-z0-9-]*$/.test(vendor)) return undefined;
|
|
128
|
-
const installed = installedVendorPacks(from).get(vendor);
|
|
120
|
+
const installed = installedVendorPacks(from, { vendors: [vendor] }).get(vendor);
|
|
129
121
|
const repo = packRepository(from);
|
|
130
122
|
const checkout = repo === undefined ? undefined : join(repo, vendor);
|
|
131
123
|
if (!checkout || packageName(checkout) !== `@volter/twin-${vendor}` || !isDerivedPack(checkout)) return installed;
|
package/src/packRegistry.ts
CHANGED
|
@@ -166,13 +166,16 @@ export async function pullOnSchedule<T>(vendor: PullVendor, pull: () => Promise<
|
|
|
166
166
|
* matches and no exclude does. Validated by scripts/pack-facts.ts: keys are `[a-z0-9-]+`, a
|
|
167
167
|
* foreign key is never another pack's vendor id, belongs to one pack, and must appear in the
|
|
168
168
|
* pack's `adoption.worldIds` (the world wires `<KEY>_TWIN_URL` from there); a pack must claim its
|
|
169
|
-
* own vendor id too; no key is exclude-only; `exclude` is `true` or absent.
|
|
169
|
+
* own vendor id too; no key is exclude-only; `exclude` is `true` or absent. `basic` is a vendor fact for the executor: on
|
|
170
|
+
* a URL the rule names, the host takes the sealed token as HTTP Basic's password under this user (a vendor's git over
|
|
171
|
+
* HTTP whose API takes the same token as a bearer; ADR 0031).
|
|
170
172
|
*/
|
|
171
173
|
export type HostRule = ({ host: string } | { suffix: string } | { hostPattern: string }) & {
|
|
172
174
|
pathPattern?: string;
|
|
173
175
|
demandPathPattern?: string | false;
|
|
174
176
|
key?: string;
|
|
175
177
|
exclude?: true;
|
|
178
|
+
basic?: string;
|
|
176
179
|
};
|
|
177
180
|
|
|
178
181
|
export type RoundTripWrite = { method: string; path: string; body?: unknown; headers?: Record<string, string> };
|
|
@@ -234,6 +237,11 @@ export type TwinPack = {
|
|
|
234
237
|
managedService?: { kind: string; scheme: string; note: string; configurationArg?: '--configuration'; originalInputs?: { version: 1; manifestArg: '--original-inputs'; rootArg: '--original-root'; digestArg: '--original-inputs-sha256'; paths: string[] } };
|
|
235
238
|
managedDatabase?: { kind: 'postgres'; arg: string; user: string; database: string; clock: 'wall'; credentialField: string; note: string };
|
|
236
239
|
sidePorts?: Array<{ name: string; option: string; flag: string; path?: string; scheme?: string; note: string }>;
|
|
240
|
+
/** The vendor's own requests a World's generated seed makes for what its world.json declares (ADR 0033 §3), each a URL
|
|
241
|
+
* at the vendor's own host, defined by the twin's own screen form for that act: its sign-up, and its domain screen's
|
|
242
|
+
* add and verify with `record`, the one `/_twin/` stand-in, for a record at the domain's DNS host; `settles` is the
|
|
243
|
+
* World-clock time the vendor takes to settle a verified domain (`10m`). */
|
|
244
|
+
seed?: { signUp?: string; domain?: { add: string; record: string; verify: string; settles: string } };
|
|
237
245
|
stateSystem?: StateSystemAdapters;
|
|
238
246
|
/** The pack has no vendor-backed half, and why (copied from the manifest by packOf). */
|
|
239
247
|
vendorBacked?: { none: string };
|
package/src/request-body.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The default is Bun's existing largest plain body (128 MiB), so both runtimes decode the same.
|
|
1
|
+
/** The shared HTTP transport and compressed-request output retain Bun's 128 MiB plain-body default.
|
|
3
2
|
* Source: bun-types/serve.d.ts, ServeOptions.maxRequestBodySize (@default 1024 * 1024 * 128),
|
|
4
3
|
* https://github.com/oven-sh/bun/blob/main/packages/bun-types/serve.d.ts .
|
|
5
|
-
* A pack's descriptor.maxRequestBodySize overrides
|
|
4
|
+
* A pack's descriptor.maxRequestBodySize overrides both the transport and decoded-output limits. */
|
|
6
5
|
export const DEFAULT_MAX_DECODED_REQUEST_BODY_SIZE = 128 * 1024 * 1024;
|
|
7
6
|
|
|
8
7
|
export class RequestBodyTooLargeError extends Error {
|
package/src/runtime.ts
CHANGED
|
@@ -16,11 +16,12 @@ export { ORIGINAL_PATH_HEADER } from './sigv4.ts';
|
|
|
16
16
|
export { createTwinFetchFromHandler, placedCookies, TWIN_PREFIX_HEADER, TWIN_REQUEST_SCOPES, TWIN_SITES_HEADER, TWIN_SITE_HEADER, siteLabel, siteUrlOf, twinPublicBase, twinSiteUrl, twinVendorUrl, withRequestScopes } from './twin-fetch.ts';
|
|
17
17
|
export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runAsReadOnlyRequest, runAsVendorMove, writesRefused } from './request-scope.ts';
|
|
18
18
|
export { compileSurface, createDerivedFetch, matchOperation, observeDerivedDispatch } from './derived.ts';
|
|
19
|
+
export { hostOf, laneSelector, slashesMerged, unitRequestRouter, worldDoorMatcher } from './pack-fetch.ts';
|
|
19
20
|
export { currentTraceparent, deliveryTraceHeaders, newTraceparent, parseTraceparent, runWithRequestTrace, runWithTraceparent, TRACEPARENT_HEADER, traceparentForDelivery, validTraceparent } from './trace-context.ts';
|
|
20
21
|
export type { Traceparent } from './trace-context.ts';
|
|
21
22
|
export type { DerivedCall, DerivedCoreOutcome, DerivedDispatch, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface, BoardFrame } from './derived.ts';
|
|
22
23
|
export { authRefusal, bindSemantics, coreFor, crossCutting, flagOf, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from './derived-core.ts';
|
|
23
|
-
export { deliverEvents, fillEnvelope, setDeliveryObserver, signEvent, signEventAll, verifyEvent } from './events.ts';
|
|
24
|
+
export { deliverEvents, fillEnvelope, setDeliveryObserver, settleEventDeliveries, signEvent, signEventAll, verifyEvent } from './events.ts';
|
|
24
25
|
export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtSignClaims, jwtVerify, jwtVerifySignature, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from './signing.ts';
|
|
25
26
|
export type { HandlerCrypto, Jwk, Jwks, JwtDecodeOptions, JwtSignatureOptions, JwtVerdict, JwtVerificationKey, SigningKey } from './signing.ts';
|
|
26
27
|
export type { EventDelivery, EventRender, EventScheme, EventsDecl, EventTransport, EventWrite } from './events.ts';
|
|
@@ -306,6 +307,8 @@ export * as git from './git/index.ts';
|
|
|
306
307
|
export * as s3 from './s3/wire.ts';
|
|
307
308
|
// Generic strict XML grammar; vendor interpretation remains in its consumer.
|
|
308
309
|
export * as xml from './xml.ts';
|
|
310
|
+
// Generic WHATWG HTML grammar; what a pack keeps of received HTML remains the pack's policy.
|
|
311
|
+
export * as html from './html.ts';
|
|
309
312
|
|
|
310
313
|
// The push arm: the kernel's transaction over a pack's one `perform<Name>Action` (contract section of that name).
|
|
311
314
|
|
|
@@ -342,6 +345,7 @@ export { brandTokensResponse } from './brand-tokens.ts';
|
|
|
342
345
|
export { multipartBoundary, multipartParts, multipartEnvelope, inferMultipartBoundary, multipartDelimiterPattern, type MultipartPart, type MultipartFramedPart, type MultipartEnvelope } from './multipart.ts';
|
|
343
346
|
export { corsHeaders, withCors, type CorsDecl } from './cors.ts';
|
|
344
347
|
export { setInProcessTwins, vendorFetch, VendorUnreachableError, type InProcessTwin } from './vendor-call.ts';
|
|
348
|
+
export { setInProcessMailServers } from './smtp.ts';
|
|
345
349
|
export { closeManagedDatabases, databaseSession, EngineDatabaseError, EngineUnavailableError, managedDatabase, quoteIdent, useDatabaseDriver, type DatabaseDriver, type DatabaseSession } from './managed-database.ts';
|
|
346
350
|
export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.ts';
|
|
347
351
|
export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob, writeResourceBlobStream } from './resource-blob.ts';
|