@volter/world-core 3.0.125 → 3.0.127

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.
@@ -1151,6 +1151,8 @@ export declare function coreFor(m: DerivedManifest, scope?: CoreScope): {
1151
1151
  /** The header the kernel names a scenario's decision in, set by createPackFetch alone (it strips it from every request it
1152
1152
  * is sent). */
1153
1153
  export declare const SCENARIO_DECISION_HEADER = "x-volter-scenario-decision";
1154
+ /** HTTP field values are bytes; JSON escapes retain Unicode without putting it directly in a header. */
1155
+ export declare function scenarioDecisionHeader(decision: ScenarioDecision): string;
1154
1156
  export declare function authRefusal(m: DerivedManifest, request: Request, root?: string): Response | undefined;
1155
1157
  export declare function crossCutting(m: DerivedManifest, opts?: CoreScope & {
1156
1158
  readOnly?: boolean;
@@ -697,6 +697,8 @@ function recordedInput(m, call, merged) {
697
697
  }
698
698
  /** The scope each call is served in (serveCore, contextFor), for the contexts a write opens for its hooks. */
699
699
  const scopeOfCall = new WeakMap();
700
+ /** The intact copy contextFor already keeps for core(), before a handler can consume its request. */
701
+ const requestOfCall = new WeakMap();
700
702
  async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
701
703
  const before = stateBeforeWrite(m, resource, id, root);
702
704
  const withFiles = async (input) => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
@@ -736,16 +738,17 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
736
738
  const body = view(m, resource, row);
737
739
  // the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
738
740
  // the managed database), at the write's moment
739
- const context = () => contextFor(m, { ...call, request: call.request.clone() }, { ...(scopeOfCall.get(call) ?? {}), ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
741
+ const request = requestOfCall.get(call) ?? call.request;
742
+ const context = () => contextFor(m, { ...call, request: request.clone() }, { ...(scopeOfCall.get(call) ?? {}), ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
740
743
  if (m.onWrite)
741
- await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request, context });
744
+ await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone(), context });
742
745
  if (m.events) {
743
746
  const events = m.events;
744
- const write = { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request };
747
+ const write = { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
745
748
  await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined);
746
749
  }
747
750
  if (m.sockets?.length)
748
- socketWrite(m.service, root, { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request });
751
+ socketWrite(m.service, root, { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
749
752
  // a read held until the World writes (held-reads.ts) hears it
750
753
  heldReadsWrite(m.service, root, storedType(m, resource));
751
754
  // the subject's id as it stands after the write: the vendor's, when a live head minted one
@@ -1311,6 +1314,7 @@ export async function contextFor(m, call, scope) {
1311
1314
  // every read of the body is of a copy, so the handler's own `call.request` and the core's answer (`core()`) each
1312
1315
  // still find it unread, whichever of them reads it first
1313
1316
  const pristine = call.request.clone();
1317
+ requestOfCall.set(call, pristine);
1314
1318
  const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root).catch((error) => {
1315
1319
  // An around context opens before the credential/JSON guard. Keep non-body parameters available to it without
1316
1320
  // turning the guard's declared malformed-body refusal into a context-construction exception.
@@ -1582,6 +1586,10 @@ const hex = (s) => Array.from(new TextEncoder().encode(s), (b) => b.toString(16)
1582
1586
  /** The header the kernel names a scenario's decision in, set by createPackFetch alone (it strips it from every request it
1583
1587
  * is sent). */
1584
1588
  export const SCENARIO_DECISION_HEADER = 'x-volter-scenario-decision';
1589
+ /** HTTP field values are bytes; JSON escapes retain Unicode without putting it directly in a header. */
1590
+ export function scenarioDecisionHeader(decision) {
1591
+ return JSON.stringify(decision).replace(/[\u007f-\uffff]/g, (unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`);
1592
+ }
1585
1593
  function scenarioDecisionOf(request) {
1586
1594
  const raw = request.headers.get(SCENARIO_DECISION_HEADER);
1587
1595
  if (!raw)
@@ -11,7 +11,7 @@ import { ORIGINAL_PATH_HEADER } from "./sigv4.js";
11
11
  import { placedCookies, TWIN_PREFIX_HEADER } from "./twin-fetch.js";
12
12
  import { PREFIX_PARAMS_HEADER } from "./derived.js";
13
13
  import { withHeaders } from "./with-headers.js";
14
- import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, coreFor, crossCutting, derivedRequestScopes, vendorError } from "./derived-core.js";
14
+ import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, coreFor, crossCutting, derivedRequestScopes, vendorError } from "./derived-core.js";
15
15
  import { compileSurface, createDerivedFetch, matchOperation, pathParams } from "./derived.js";
16
16
  import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from "./request-scope.js";
17
17
  import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
@@ -220,7 +220,7 @@ export function createPackFetch(parts, options = {}) {
220
220
  }
221
221
  if (decision.kind === 'fault')
222
222
  return Response.json(decision.result.body, { status: decision.result.status, headers: decision.result.headers ?? {} });
223
- return api(withHeaders(request, new Headers([...request.headers, [SCENARIO_DECISION_HEADER, JSON.stringify(decision)]])));
223
+ return api(withHeaders(request, new Headers([...request.headers, [SCENARIO_DECISION_HEADER, scenarioDecisionHeader(decision)]])));
224
224
  };
225
225
  const doors = (m.doors ?? []).map((d) => {
226
226
  const answer = parts.doors?.[d.id];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "3.0.125",
3
+ "version": "3.0.127",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
@@ -1165,6 +1165,8 @@ function recordedInput(m: DerivedManifest, call: DerivedCall, merged: Record<str
1165
1165
 
1166
1166
  /** The scope each call is served in (serveCore, contextFor), for the contexts a write opens for its hooks. */
1167
1167
  const scopeOfCall = new WeakMap<DerivedCall, CoreScope>();
1168
+ /** The intact copy contextFor already keeps for core(), before a handler can consume its request. */
1169
+ const requestOfCall = new WeakMap<DerivedCall, Request>();
1168
1170
 
1169
1171
  async function writeDetailed(m: DerivedManifest, call: DerivedCall, resource: string, id: string, fields: Record<string, unknown>, operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string, checks?: StateChecks): Promise<{ body: Record<string, unknown>; id: string; vendorData?: unknown }> {
1170
1172
  const before = stateBeforeWrite(m, resource, id, root);
@@ -1215,14 +1217,15 @@ async function finishWrite(m: DerivedManifest, call: DerivedCall, resource: stri
1215
1217
  const body = view(m, resource, row);
1216
1218
  // the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
1217
1219
  // the managed database), at the write's moment
1218
- const context = (): Promise<SemanticsContext> => contextFor(m, { ...call, request: call.request.clone() }, { ...(scopeOfCall.get(call) ?? {}), ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
1219
- if (m.onWrite) await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request, context });
1220
+ const request = requestOfCall.get(call) ?? call.request;
1221
+ const context = (): Promise<SemanticsContext> => contextFor(m, { ...call, request: request.clone() }, { ...(scopeOfCall.get(call) ?? {}), ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
1222
+ if (m.onWrite) await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone(), context });
1220
1223
  if (m.events) {
1221
1224
  const events = m.events;
1222
- const write = { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request };
1225
+ const write = { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
1223
1226
  await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values!(await context(), write, type) : undefined);
1224
1227
  }
1225
- if (m.sockets?.length) socketWrite(m.service, root, { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request });
1228
+ if (m.sockets?.length) socketWrite(m.service, root, { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
1226
1229
  // a read held until the World writes (held-reads.ts) hears it
1227
1230
  heldReadsWrite(m.service, root, storedType(m, resource));
1228
1231
  // the subject's id as it stands after the write: the vendor's, when a live head minted one
@@ -1929,6 +1932,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
1929
1932
  // every read of the body is of a copy, so the handler's own `call.request` and the core's answer (`core()`) each
1930
1933
  // still find it unread, whichever of them reads it first
1931
1934
  const pristine = call.request.clone();
1935
+ requestOfCall.set(call, pristine);
1932
1936
  const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root).catch((error) => {
1933
1937
  // An around context opens before the credential/JSON guard. Keep non-body parameters available to it without
1934
1938
  // turning the guard's declared malformed-body refusal into a context-construction exception.
@@ -2184,6 +2188,10 @@ const hex = (s: string): string => Array.from(new TextEncoder().encode(s), (b) =
2184
2188
  /** The header the kernel names a scenario's decision in, set by createPackFetch alone (it strips it from every request it
2185
2189
  * is sent). */
2186
2190
  export const SCENARIO_DECISION_HEADER = 'x-volter-scenario-decision';
2191
+ /** HTTP field values are bytes; JSON escapes retain Unicode without putting it directly in a header. */
2192
+ export function scenarioDecisionHeader(decision: ScenarioDecision): string {
2193
+ return JSON.stringify(decision).replace(/[\u007f-\uffff]/g, (unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`);
2194
+ }
2187
2195
  function scenarioDecisionOf(request: Request): ScenarioDecision | undefined {
2188
2196
  const raw = request.headers.get(SCENARIO_DECISION_HEADER);
2189
2197
  if (!raw) return undefined;
package/src/pack-fetch.ts CHANGED
@@ -11,7 +11,7 @@ import { ORIGINAL_PATH_HEADER } from './sigv4.ts';
11
11
  import { placedCookies, TWIN_PREFIX_HEADER } from './twin-fetch.ts';
12
12
  import { PREFIX_PARAMS_HEADER } from './derived.ts';
13
13
  import { withHeaders } from './with-headers.ts';
14
- import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type VendorManifest } from './derived-core.ts';
14
+ import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type VendorManifest } from './derived-core.ts';
15
15
  import { compileSurface, createDerivedFetch, matchOperation, pathParams, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.ts';
16
16
  import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from './request-scope.ts';
17
17
  import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest, type PackScenarioAdapter } from './scenario.ts';
@@ -274,7 +274,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
274
274
  return vendorError(m, { status: 500, code: 'scenario_error', message: `the World's scenario: ${error.message}` });
275
275
  }
276
276
  if (decision.kind === 'fault') return Response.json(decision.result.body, { status: decision.result.status, headers: decision.result.headers ?? {} });
277
- return api(withHeaders(request, new Headers([...request.headers, [SCENARIO_DECISION_HEADER, JSON.stringify(decision)]])));
277
+ return api(withHeaders(request, new Headers([...request.headers, [SCENARIO_DECISION_HEADER, scenarioDecisionHeader(decision)]])));
278
278
  };
279
279
  const doors = (m.doors ?? []).map((d) => {
280
280
  const answer = parts.doors?.[d.id];