@volter/world-core 2.0.0 → 2.0.2

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.
Files changed (66) hide show
  1. package/app-route.cjs +95 -6
  2. package/app-route.d.cts +2 -1
  3. package/dist/app-route.cjs +95 -6
  4. package/dist/app-route.d.cts +2 -1
  5. package/dist/generated/pack-facts.json +127 -8
  6. package/dist/inject.cjs +46 -1
  7. package/dist/src/actions.d.ts +27 -0
  8. package/dist/src/actions.js +62 -4
  9. package/dist/src/blob-store.d.ts +3 -0
  10. package/dist/src/blob-store.js +15 -1
  11. package/dist/src/client-bundle.d.ts +4 -0
  12. package/dist/src/client-bundle.js +13 -2
  13. package/dist/src/derived-core.d.ts +6 -0
  14. package/dist/src/derived-core.js +17 -3
  15. package/dist/src/derived.js +5 -2
  16. package/dist/src/git/refs.js +5 -3
  17. package/dist/src/head.js +12 -2
  18. package/dist/src/index.d.ts +8 -5
  19. package/dist/src/index.js +9 -5
  20. package/dist/src/log.d.ts +2 -0
  21. package/dist/src/packRegistry.d.ts +8 -6
  22. package/dist/src/redis/engine.d.ts +308 -0
  23. package/dist/src/redis/engine.js +1663 -0
  24. package/dist/src/redis/index.d.ts +2 -0
  25. package/dist/src/redis/index.js +6 -0
  26. package/dist/src/redis/lua.d.ts +153 -0
  27. package/dist/src/redis/lua.js +1373 -0
  28. package/dist/src/request-scope.d.ts +28 -0
  29. package/dist/src/request-scope.js +70 -0
  30. package/dist/src/storage.d.ts +22 -1
  31. package/dist/src/storage.js +57 -1
  32. package/dist/src/trace-context.d.ts +31 -0
  33. package/dist/src/trace-context.js +78 -0
  34. package/dist/src/twin-fetch.d.ts +16 -0
  35. package/dist/src/twin-fetch.js +66 -3
  36. package/dist/src/world-clock.d.ts +2 -2
  37. package/dist/src/world-clock.js +18 -17
  38. package/dist/src/world-store.js +9 -0
  39. package/dist/vendor-hosts.cjs +2 -2
  40. package/dist/world-clock.cjs +40 -0
  41. package/dist/world-clock.d.cts +4 -0
  42. package/generated/pack-facts.json +127 -8
  43. package/inject.cjs +46 -1
  44. package/package.json +11 -1
  45. package/src/actions.ts +69 -4
  46. package/src/blob-store.ts +15 -1
  47. package/src/client-bundle.ts +15 -2
  48. package/src/derived-core.ts +17 -3
  49. package/src/derived.ts +5 -2
  50. package/src/git/refs.ts +5 -3
  51. package/src/head.ts +11 -2
  52. package/src/index.ts +11 -3
  53. package/src/log.ts +2 -0
  54. package/src/packRegistry.ts +8 -6
  55. package/src/redis/engine.ts +1468 -0
  56. package/src/redis/index.ts +6 -0
  57. package/src/redis/lua.ts +1250 -0
  58. package/src/request-scope.ts +74 -0
  59. package/src/storage.ts +54 -2
  60. package/src/trace-context.ts +87 -0
  61. package/src/twin-fetch.ts +69 -3
  62. package/src/world-clock.ts +19 -16
  63. package/src/world-store.ts +9 -0
  64. package/vendor-hosts.cjs +2 -2
  65. package/world-clock.cjs +40 -0
  66. package/world-clock.d.cts +4 -0
@@ -16,10 +16,12 @@ import { AsyncLocalStorage } from 'node:async_hooks';
16
16
  import { randomUUID } from 'node:crypto';
17
17
  import { dirname, join } from 'node:path';
18
18
  import { canonicalJson, subjectKey } from "./hash.js";
19
- import { appendDurable, eventsLockPath, projectionLockPath, twinLog, withFileLock, worldPaths } from "./storage.js";
19
+ import { appendDurable, eventsLockPath, ownerStoreRoots, projectionLockPath, twinLog, withFileLock, withoutIdentityClaim, worldPaths } from "./storage.js";
20
20
  import { landAsPlaceholder, placeholderPullActive } from "./placeholder-remote.js";
21
- import { aliasesFrom, dropCheckpoint, landedCopy, landedIds, parentEntries, readTree } from "./log.js";
21
+ import { aliasesFrom, dropCheckpoint, landedCopy, landedIds, parentEntries, readTree, treeStamp } from "./log.js";
22
+ import { refuseReadOnlyWrite } from "./request-scope.js";
22
23
  import { getActiveWorldStore } from "./world-store.js";
24
+ import { currentTraceparent } from "./trace-context.js";
23
25
  export class TwinActionPreconditionError extends Error {
24
26
  actionId;
25
27
  failed;
@@ -43,7 +45,10 @@ function withCorrelationId(action) {
43
45
  // The wire's request id (the addressed service stamps every response with one and threads it
44
46
  // into the handler's async context) is the correlation when the caller supplies none — the
45
47
  // join from a request on the wire to the action rows it caused, with no pack involved.
46
- return action.correlationId ? action : { ...action, correlationId: currentCorrelationId() ?? randomUUID() };
48
+ const correlated = action.correlationId ? action : { ...action, correlationId: currentCorrelationId() ?? randomUUID() };
49
+ // the request's W3C trace context (trace-context.ts) is stamped the same way, from the handler's async context
50
+ const traceparent = correlated.traceparent === undefined ? currentTraceparent() : undefined;
51
+ return traceparent ? { ...correlated, traceparent } : correlated;
47
52
  }
48
53
  /** Subjects a `set` action touches: its own subject, every projection resource, and every
49
54
  * precondition subject — a precondition established the action's validity against that
@@ -114,7 +119,7 @@ function landIfPlaceholderPull(action, root) {
114
119
  return { ...action, id: events.at(-1)?.id ?? action.id };
115
120
  }
116
121
  function replayBody(action) {
117
- const { correlationId: _correlationId, ...stampedBody } = action;
122
+ const { correlationId: _correlationId, traceparent: _traceparent, ...stampedBody } = action;
118
123
  // A confirmation is content-addressed by the observed event ids. Its timestamp is observation
119
124
  // metadata, just like the occurredAt/observedAt values that appendEvent ignores when replaying
120
125
  // one content-addressed event. Two reconcilers confirming the same bytes at different wall-clock
@@ -145,6 +150,7 @@ const correlationScope = () => (correlationStore ??= new AsyncLocalStorage());
145
150
  export function runWithCorrelationId(id, fn) { return correlationScope().run(id, fn); }
146
151
  export function currentCorrelationId() { return correlationScope().getStore(); }
147
152
  export function appendAction(action, root) {
153
+ refuseReadOnlyWrite(action.service); // a read-only request writes nothing (request-scope.ts)
148
154
  // Evaluate and append under the service projection and actions locks. A precondition is a
149
155
  // compare-and-set, not an advisory validation: checking outside either lock would let another
150
156
  // writer invalidate it before this action lands. The shadow basis is stamped under the same
@@ -164,6 +170,7 @@ export function appendAction(action, root) {
164
170
  * atomic across processes (two concurrent identical writes converge to ONE action; distinct
165
171
  * writes both land). A reused id with different content fails loudly. */
166
172
  export function appendActionIfAbsent(action, root) {
173
+ refuseReadOnlyWrite(action.service);
167
174
  return withFileLock(projectionLock(action.service, root), () => withFileLock(actionsLock(action.service, root), () => {
168
175
  const identified = withCorrelationId(action);
169
176
  // An exact retry is already committed. Resolve it before re-evaluating author-time
@@ -198,6 +205,7 @@ export function appendActionIfAbsent(action, root) {
198
205
  * the same ordinals, which is what serve-path determinism requires.
199
206
  */
200
207
  export function appendActionOccurrence(base, root) {
208
+ refuseReadOnlyWrite(base.service);
201
209
  return withFileLock(projectionLock(base.service, root), () => withFileLock(actionsLock(base.service, root), () => {
202
210
  const stamped = occurrenceOf(base, actionIds(base.service, root), root);
203
211
  assertPreconditions(stamped, root);
@@ -241,6 +249,7 @@ export function decideAndAppendAction(service, decide, root) {
241
249
  const decision = decide(resources);
242
250
  if (decision.kind === 'skip')
243
251
  return { value: decision.value, appended: false };
252
+ refuseReadOnlyWrite(service); // a decision to write is the write a read-only request may not make
244
253
  if (decision.action.service !== service) {
245
254
  throw new Error(`Atomic action service mismatch: expected ${service}, got ${decision.action.service}`);
246
255
  }
@@ -334,6 +343,53 @@ function assertPreconditions(action, root) {
334
343
  export function projectResources(service, root, opts = {}) {
335
344
  return readTree(service, root, opts);
336
345
  }
346
+ /** A read of another pack's store that cannot choose: the subject it looks for (or, with none named, the store itself)
347
+ * is held by more than one service of the World. A reader answers it as the vendor answers a credential it cannot
348
+ * resolve; it is never a server error. */
349
+ export class OwnerStoreAmbiguousError extends Error {
350
+ constructor(owner, roots, subject) {
351
+ super(`${subject ? `${subject.type} ${subject.id} of ` : ''}${owner}'s store is held by more than one service of this World (${roots.join(', ')}); a cross-pack read cannot choose between them`);
352
+ this.name = 'OwnerStoreAmbiguousError';
353
+ }
354
+ }
355
+ const ownerIndexes = new Map();
356
+ function ownerIndex(owner, root) {
357
+ const key = `${owner}\0${root}`;
358
+ const stamp = treeStamp(owner, root);
359
+ const held = ownerIndexes.get(key);
360
+ if (held && held.stamp === stamp)
361
+ return held;
362
+ const rows = readTree(owner, root).map((r) => Object.freeze(r));
363
+ const index = { stamp, rows: Object.freeze(rows), byKey: new Map(rows.map((r) => [`${r.type}:${r.id}`, r])) };
364
+ ownerIndexes.set(key, index);
365
+ return index;
366
+ }
367
+ /**
368
+ * ANOTHER pack's rows, read by contract (architecture A3: one vendor's store split across two packs, xidentity's tokens
369
+ * read by x, googleoauth's by googlecalendar; scripts/architecture.test.ts holds the declared reader-owner pairs): the
370
+ * owner's tree, from wherever the World keeps the owner's store (`ownerStoreRoots`). Read only: the rows are frozen and
371
+ * shared. It claims no journal identity (the twin answering is the reader).
372
+ *
373
+ * With `subject`, the store that holds that subject (the token a request presents): `[]` when none does, and
374
+ * `OwnerStoreAmbiguousError` only when more than one does, so a second service running the owning pack never breaks a
375
+ * lookup of what only one of them issued. Without it, the one store there is, and `OwnerStoreAmbiguousError` when the
376
+ * World holds two.
377
+ */
378
+ export function projectOwnerResources(owner, root, subject) {
379
+ return withoutIdentityClaim(() => {
380
+ const roots = ownerStoreRoots(owner, root);
381
+ if (!subject) {
382
+ if (roots.length > 1)
383
+ throw new OwnerStoreAmbiguousError(owner, roots);
384
+ return roots[0] ? ownerIndex(owner, roots[0]).rows : [];
385
+ }
386
+ const key = `${subject.type}:${subject.id}`;
387
+ const holding = roots.filter((r) => ownerIndex(owner, r).byKey.has(key));
388
+ if (holding.length > 1)
389
+ throw new OwnerStoreAmbiguousError(owner, holding, subject);
390
+ return holding[0] ? ownerIndex(owner, holding[0]).rows : [];
391
+ });
392
+ }
337
393
  /** The local → vendor id aliases the landed copies carry: a read by the id a caller was handed before
338
394
  * its write was performed resolves to the row the vendor now owns. */
339
395
  export function subjectAliases(service, root) {
@@ -354,6 +410,7 @@ export function resolveSubjectId(service, type, id, root) {
354
410
  * the change is counted exactly once. Returns the observed event id.
355
411
  */
356
412
  export function confirmAction(opts) {
413
+ refuseReadOnlyWrite(opts.service);
357
414
  // LANDING (log.ts): the entry is copied to the parent log with its receipt — no confirm row, no
358
415
  // suppression; the fold skips a branch entry the parent holds.
359
416
  const paths = worldPaths(opts.service, opts.root);
@@ -384,6 +441,7 @@ export function confirmAction(opts) {
384
441
  * Only `set` rows are revertable — reverting a revert or a confirm is a category error.
385
442
  */
386
443
  export function revertAction(opts) {
444
+ refuseReadOnlyWrite(opts.service);
387
445
  return withFileLock(projectionLock(opts.service, opts.root), () => withFileLock(actionsLock(opts.service, opts.root), () => {
388
446
  const all = listActions(opts.service, opts.root);
389
447
  const target = all.find((a) => a.id === opts.actionId);
@@ -50,6 +50,9 @@ export declare class MemoryBlobStore implements BlobStore {
50
50
  }
51
51
  /** Content-address helper shared by byte-carrying packs: sha256 hex of the bytes. */
52
52
  export declare function blobDigest(bytes: Uint8Array): string;
53
+ /** The active store; under a read-only request's refusing scope (request-scope.ts), one that refuses
54
+ * to store or remove bytes before any reach it, as the log's appenders refuse (a git push's objects,
55
+ * an upload's body), while reads pass. */
53
56
  export declare function getActiveBlobStore(): BlobStore;
54
57
  export declare function setActiveBlobStore(store: BlobStore): BlobStore;
55
58
  export declare function withBlobStore<T>(store: BlobStore, fn: () => Promise<T> | T): Promise<T>;
@@ -23,6 +23,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync,
23
23
  import { dirname, join } from 'node:path';
24
24
  import { volterHome } from "./volter-home.js";
25
25
  import { sharedBlobIndex } from "./shared-blob-index.js";
26
+ import { refuseReadOnlyWrite, writesRefused } from "./request-scope.js";
26
27
  /** A range of the blob at `key` through the active store: native when the store has it, else sliced. */
27
28
  export async function readBlobRange(key, start, endInclusive) {
28
29
  // one clamping for every backend: a negative or non-finite start reads from 0, a range that ends
@@ -167,8 +168,21 @@ export function blobDigest(bytes) {
167
168
  // Active-store scoping, the world-store.ts pattern exactly: default fs; a serverless entry
168
169
  // (or a test) swaps in its adapter for the scope of a request.
169
170
  let activeBlobStore = new FsBlobStore();
171
+ /** The active store; under a read-only request's refusing scope (request-scope.ts), one that refuses
172
+ * to store or remove bytes before any reach it, as the log's appenders refuse (a git push's objects,
173
+ * an upload's body), while reads pass. */
170
174
  export function getActiveBlobStore() {
171
- return activeBlobStore;
175
+ return writesRefused() ? readOnlyBlobs(activeBlobStore) : activeBlobStore;
176
+ }
177
+ function readOnlyBlobs(store) {
178
+ return {
179
+ get: (key) => store.get(key),
180
+ exists: (key) => store.exists(key),
181
+ list: (prefix) => store.list(prefix),
182
+ size: (key) => store.size(key),
183
+ put: async () => { refuseReadOnlyWrite('blobs'); },
184
+ remove: async () => { refuseReadOnlyWrite('blobs'); },
185
+ };
172
186
  }
173
187
  export function setActiveBlobStore(store) {
174
188
  const previous = activeBlobStore;
@@ -1 +1,5 @@
1
+ /** A file URL's path on this machine, without `node:url` (the modules that call it ride into browser
2
+ * bundles): percent-escapes decoded, and without the slash `URL.pathname` puts before a Windows drive
3
+ * (`/C:/…`), which no file API opens. `new URL('../client/x.css', import.meta.url).pathname` is this. */
4
+ export declare function filePathOf(url: URL): string;
1
5
  export declare function bundleClient(entry: string): Promise<string>;
@@ -4,7 +4,18 @@
4
4
  // by scripts/publish/build.mjs into dist/client/. Built once per process and kept; a failed build
5
5
  // is retried on the next call. Nothing here runs at module scope.
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
+ /** A file URL's path on this machine, without `node:url` (the modules that call it ride into browser
8
+ * bundles): percent-escapes decoded, and without the slash `URL.pathname` puts before a Windows drive
9
+ * (`/C:/…`), which no file API opens. `new URL('../client/x.css', import.meta.url).pathname` is this. */
10
+ export function filePathOf(url) {
11
+ const path = decodeURIComponent(url.pathname);
12
+ return /^\/[A-Za-z]:\//.test(path) ? path.slice(1) : path;
13
+ }
7
14
  const bundles = new Map();
15
+ /** A browser has no `process`. A client that imports shared helpers from its pack's server-side module can pull Node
16
+ * code into its bundle (util.deprecate, crypto shims) that reads it; without a shim the page throws before it renders
17
+ * (newer Bun no longer adds one to a browser build). A minimal one, the page's own, before the bundle. */
18
+ const PROCESS_SHIM = 'globalThis.process=globalThis.process||{env:{NODE_ENV:"production"},browser:true,argv:[],versions:{},platform:"browser",emit:function(){return false},on:function(){},nextTick:function(f){var a=[].slice.call(arguments,1);queueMicrotask(function(){f.apply(null,a)})}};\n';
8
19
  export function bundleClient(entry) {
9
20
  let pending = bundles.get(entry);
10
21
  if (!pending) {
@@ -13,13 +24,13 @@ export function bundleClient(entry) {
13
24
  ? bun.build({ entrypoints: [entry], target: 'browser', minify: true }).then(async (result) => {
14
25
  if (!result.success)
15
26
  throw new Error(result.logs.map((l) => l.message).join('\n') || `client build failed: ${entry}`);
16
- return result.outputs[0].text();
27
+ return PROCESS_SHIM + await result.outputs[0].text();
17
28
  })
18
29
  : Promise.resolve().then(() => {
19
30
  const prebuilt = entry.replace(/\.tsx?$/, '.bundle.js');
20
31
  if (!existsSync(prebuilt))
21
32
  throw new Error(`client bundle missing at ${prebuilt} — the package's \`build\` writes it (scripts/publish/build.mjs); Node serves the prebuilt client`);
22
- return readFileSync(prebuilt, 'utf8');
33
+ return PROCESS_SHIM + readFileSync(prebuilt, 'utf8');
23
34
  }))
24
35
  .catch((error) => { bundles.delete(entry); throw error; });
25
36
  bundles.set(entry, pending);
@@ -447,6 +447,12 @@ export declare function coreFor(m: DerivedManifest, scope?: CoreScope): {
447
447
  export declare function crossCutting(m: DerivedManifest, opts?: CoreScope & {
448
448
  readOnly?: boolean;
449
449
  }): (call: DerivedCall, next: () => Promise<Response>) => Promise<Response>;
450
+ /** A derived pack's whole fetch behind the kernel's read scope (twin-fetch.ts withRequestScopes): a
451
+ * read-only request's writes are refused at the write seam — the ones `crossCutting` cannot see
452
+ * coming (another wire's mutation, a door, a screen) — and answered with the vendor's own read-only
453
+ * error; `GET /twin` advertises `requestScopes`. The pack's catch-up runs under `runAsVendorMove`
454
+ * (request-scope.ts), so time's moves still land when a read-only request is what arrives. */
455
+ export declare function derivedRequestScopes<F extends (request: Request) => Promise<Response>>(m: DerivedManifest, fetch: F): F;
450
456
  /** A semantics context for a request no surface operation names: another wire's (GraphQL) resolvers
451
457
  * get the same interface as a handler, named by the operation id the wire gives. */
452
458
  export declare function semanticsContext(m: DerivedManifest, request: Request, operation: DerivedOperation, scope?: CoreScope): Promise<SemanticsContext>;
@@ -12,7 +12,8 @@ import { createHash } from 'node:crypto';
12
12
  import { hashFieldValue } from "./hash.js";
13
13
  import { resolveSubjectId, subjectAliases } from "./actions.js";
14
14
  import { packReferences } from "./references.js";
15
- import { twinPublicBase } from "./twin-fetch.js";
15
+ import { twinPublicBase, withRequestScopes } from "./twin-fetch.js";
16
+ import { isReadOnlyRequest } from "./request-scope.js";
16
17
  /** What a move to `to` stores in the field: a boolean field's value is a boolean, and a derived field's
17
18
  * value is written by the move's effects (a state name is not a timestamp), so it stores nothing itself. */
18
19
  export function storedState(decl, to) {
@@ -732,8 +733,10 @@ export function crossCutting(m, opts = {}) {
732
733
  }
733
734
  }
734
735
  // a read-only twin refuses writes: what the operation does, not the HTTP verb it came by (an RPC
735
- // wire POSTs its reads)
736
- if (opts.readOnly && !['retrieve', 'list', 'computed'].includes(call.operation.class) && !m.reads?.includes(call.operation.id))
736
+ // wire POSTs its reads). A read-only REQUEST (x-volter-read-only, request-scope.ts) is refused the
737
+ // same way, up front, on a writable twin.
738
+ const readOnly = opts.readOnly || isReadOnlyRequest(request);
739
+ if (readOnly && !['retrieve', 'list', 'computed'].includes(call.operation.class) && !m.reads?.includes(call.operation.id))
737
740
  return vendorError(m, m.readOnly);
738
741
  // a body labelled JSON that does not parse is the vendor's refusal, never a crash of the twin
739
742
  if ((request.headers.get('content-type') ?? '').includes('json') || m.body.json === 'always') {
@@ -766,6 +769,9 @@ export function crossCutting(m, opts = {}) {
766
769
  // a streamed answer replays as the stream it was; a record kept before text was kept replays its JSON
767
770
  return answer.text !== undefined ? new Response(answer.text, { status: answer.status, headers: { 'content-type': answer.contentType ?? 'application/json' } }) : Response.json(answer.body, { status: answer.status });
768
771
  }
772
+ // a read-only request replays a stored answer but records none: recording is a write
773
+ if (readOnly)
774
+ return next();
769
775
  const response = await next();
770
776
  if (m.idempotency.onlySuccess && (response.status < 200 || response.status >= 300))
771
777
  return response;
@@ -775,6 +781,14 @@ export function crossCutting(m, opts = {}) {
775
781
  return response;
776
782
  }
777
783
  }
784
+ /** A derived pack's whole fetch behind the kernel's read scope (twin-fetch.ts withRequestScopes): a
785
+ * read-only request's writes are refused at the write seam — the ones `crossCutting` cannot see
786
+ * coming (another wire's mutation, a door, a screen) — and answered with the vendor's own read-only
787
+ * error; `GET /twin` advertises `requestScopes`. The pack's catch-up runs under `runAsVendorMove`
788
+ * (request-scope.ts), so time's moves still land when a read-only request is what arrives. */
789
+ export function derivedRequestScopes(m, fetch) {
790
+ return withRequestScopes(fetch, { refuse: () => vendorError(m, m.readOnly) });
791
+ }
778
792
  /** A semantics context for a request no surface operation names: another wire's (GraphQL) resolvers
779
793
  * get the same interface as a handler, named by the operation id the wire gives. */
780
794
  export function semanticsContext(m, request, operation, scope = {}) {
@@ -5,6 +5,7 @@
5
5
  // existing fetch, which keeps serving everything not yet moved (and every route outside the spec, such
6
6
  // as the `/twin` door). `owners` says, per operation, which of the two serves it: the count a pack's
7
7
  // move is measured by. Workerd-clean: no fs, no clock, nothing at import.
8
+ import { runWithRequestTrace } from "./trace-context.js";
8
9
  class Unmodeled extends Error {
9
10
  }
10
11
  function compile(operation, basePath, spanning) {
@@ -85,7 +86,9 @@ export function createDerivedFetch(options) {
85
86
  throw new Error(`derived dispatch: handlers name operations the surface does not have: ${unknown.sort().join(', ')}`);
86
87
  const routes = compileSurface(options.surface);
87
88
  const fallback = (request, operation, reason) => options.legacy ? options.legacy(request) : Promise.resolve(options.gap(request, operation, reason));
88
- const fetch = (async (request) => {
89
+ // a valid incoming W3C traceparent scopes the dispatch, so every entry it appends records it (trace-context.ts)
90
+ const fetch = ((request) => runWithRequestTrace(request, () => dispatch(request)));
91
+ const dispatch = async (request) => {
89
92
  const url = new URL(request.url);
90
93
  const matched = options.anyMethod
91
94
  ? [...routes.keys()].map((m) => matchOperation(routes, m, url.pathname, url.searchParams, request.headers)).find(Boolean)
@@ -116,7 +119,7 @@ export function createDerivedFetch(options) {
116
119
  }
117
120
  }
118
121
  return fallback(request, matched.operation, 'no handler');
119
- });
122
+ };
120
123
  fetch.owners = () => Object.fromEntries(options.surface.operations.map((o) => [o.id, handlers[o.id] ? 'handler' : options.core?.owns(o) ? 'core' : options.legacy ? 'legacy' : 'gap']));
121
124
  return fetch;
122
125
  }
@@ -1,3 +1,4 @@
1
+ import { refuseReadOnlyWrite } from "../request-scope.js";
1
2
  export class GitRefs {
2
3
  store;
3
4
  prefix;
@@ -7,11 +8,12 @@ export class GitRefs {
7
8
  }
8
9
  path(name) { return `${this.prefix}/${name}`; }
9
10
  get(name) { const v = this.store.read(this.path(name)); return v === null ? null : v.trim(); }
10
- set(name, sha) { this.store.write(this.path(name), `${sha}\n`); }
11
- delete(name) { if (this.store.exists(this.path(name)))
11
+ // a read-only request moves no ref (request-scope.ts): refused before the store is touched
12
+ set(name, sha) { refuseReadOnlyWrite('git'); this.store.write(this.path(name), `${sha}\n`); }
13
+ delete(name) { refuseReadOnlyWrite('git'); if (this.store.exists(this.path(name)))
12
14
  this.store.remove(this.path(name)); }
13
15
  head() { const v = this.store.read(this.path('HEAD')); return v === null ? 'refs/heads/main' : v.trim().replace(/^ref: /, ''); }
14
- setHead(target) { this.store.write(this.path('HEAD'), `ref: ${target}\n`); }
16
+ setHead(target) { refuseReadOnlyWrite('git'); this.store.write(this.path('HEAD'), `ref: ${target}\n`); }
15
17
  /** Run `fn` holding the repository's ref lock (the world store's exclusive lock). */
16
18
  withLock(fn) { return this.store.withLock(this.path('refs.lock'), fn); }
17
19
  /** Every `refs/...` name with its sha, sorted by name. */
package/dist/src/head.js CHANGED
@@ -17,7 +17,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
17
17
  // record: a `deployed` copy is never performed again, a `failed` one is retried.
18
18
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
19
19
  import { homedir } from 'node:os';
20
- import { basename, dirname, join, resolve } from 'node:path';
20
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
21
21
  import { confirmAction, isTwinBookkeeping, resolveSubjectId, revertAction } from "./actions.js";
22
22
  import { openSealedCredential, sealCredential } from "./credential.js";
23
23
  import { buildRemoteExecute, validateRemoteOrigin } from "./executor.js";
@@ -101,12 +101,22 @@ export async function loadChecks(worldRoot) {
101
101
  return out;
102
102
  }
103
103
  const importCheck = async (path, name) => {
104
- const mod = (await import(__rewriteRelativeImportExtension(path)));
104
+ // Node's ESM loader takes an absolute path only as a file URL (on Windows `C:\…` reads as the scheme `c:`); Bun takes either
105
+ const mod = (await import(__rewriteRelativeImportExtension(isAbsolute(path) ? fileUrlOf(path) : path)));
105
106
  const check = mod.check ?? mod.default;
106
107
  if (!check || typeof check.run !== 'function')
107
108
  throw new Error(`${path} must export a check { name, run }`);
108
109
  return { name: check.name ?? name, run: check.run };
109
110
  };
111
+ /** An absolute path's file URL, built here: `node:url` is not in the browser bundles that also reach this module. */
112
+ const fileUrlOf = (path) => {
113
+ // a long path (`\\?\C:\…`) is the drive path it names; a UNC path (`\\server\share\…`) puts the server in the URL's host
114
+ const long = /^\\\\\?\\(?:UNC\\)?/i.exec(path);
115
+ const unc = /^\\\\\?\\UNC\\/i.test(path) || (!long && /^\\\\[^\\?]/.test(path));
116
+ const rest = (long ? path.slice(long[0].length) : unc ? path.slice(2) : path).replace(/\\/g, '/');
117
+ const encoded = rest.split('/').map((seg) => encodeURIComponent(seg).replace(/%3A/g, ':')).join('/');
118
+ return unc ? `file://${encoded}` : `file://${rest.startsWith('/') ? '' : '/'}${encoded}`;
119
+ };
110
120
  let checkLoader = importCheck;
111
121
  export function setCheckLoader(loader) { checkLoader = loader ?? importCheck; }
112
122
  /** One check file through the host's loader: what placing a check validates with. */
@@ -3,10 +3,13 @@ export type { PackTransport, PullPosture, PullTrigger, PullVendor, RoundTripWrit
3
3
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from './scenario.js';
4
4
  export { WORLD_CLOCK_ENV, worldNow } from './world-clock.js';
5
5
  export { WORLD_ENV_NAMES_ENV, worldEnvValue } from './world-env.js';
6
- export { createTwinFetchFromHandler, TWIN_PREFIX_HEADER, twinPublicBase } from './twin-fetch.js';
6
+ export { createTwinFetchFromHandler, TWIN_PREFIX_HEADER, TWIN_REQUEST_SCOPES, twinPublicBase, withRequestScopes } from './twin-fetch.js';
7
+ export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runAsReadOnlyRequest, runAsVendorMove, writesRefused } from './request-scope.js';
7
8
  export { compileSurface, createDerivedFetch, matchOperation } from './derived.js';
9
+ export { currentTraceparent, deliveryTraceHeaders, parseTraceparent, runWithRequestTrace, runWithTraceparent, TRACEPARENT_HEADER, traceparentForDelivery, validTraceparent } from './trace-context.js';
10
+ export type { Traceparent } from './trace-context.js';
8
11
  export type { DerivedCall, DerivedCoreOutcome, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface } from './derived.js';
9
- export { bindSemantics, coreFor, crossCutting, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from './derived-core.js';
12
+ export { bindSemantics, coreFor, crossCutting, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from './derived-core.js';
10
13
  export type { Actor, CoreScope, DerivedManifest, ErrorSpec, FieldRule, ResourceDecl, ScreenDecl, Semantics, SemanticsContext, StateField, Transition, TransitionObserver } from './derived-core.js';
11
14
  export type { RemoteExecute, RemoteExecuteRequest, RemoteExecuteResponse } from './remote-execute.js';
12
15
  export type { TwinFetchAdapterConfig, TwinFetchHandlerRequest, TwinFetchHandlerResult, TwinStream, TwinStreamConnection, TwinStreamSink } from './twin-fetch.js';
@@ -20,7 +23,7 @@ export { loadWorldConfig, } from './worldConfig.js';
20
23
  export type { WorldConfig, WorldServiceConfig, } from './worldConfig.js';
21
24
  export { DELTA_TYPE_SUFFIX, diffSubjectFields, hashFieldValue, subjectKey, deltaAfterFields } from './hash.js';
22
25
  export type { FieldChange, SubjectFields, } from './hash.js';
23
- export { appendDurable, appendEvent, createEvent, emptyGenericState, genericWorldReducer, listEvents, loadState, projectionLockPath, readJsonFile, rebuildGenericState, rebuildState, scrubService, scrubWorld, stateDirName, twinLog, withFileLock, worldPaths, worldStateRoot, } from './storage.js';
26
+ export { appendDurable, appendEvent, createEvent, emptyGenericState, genericWorldReducer, listEvents, loadState, projectionLockPath, readJsonFile, rebuildGenericState, rebuildState, scrubService, scrubWorld, stateDirName, twinLog, withFileLock, worldPaths, worldStateRoot, ownerStoreRoots, WORLD_DATA_ENV, } from './storage.js';
24
27
  export type { AppendEventResult, CommitQueuedEventsResult, GenericWorldState, EnqueueEventResult, QueuedWorldServiceEvent, WorldPaths, WorldReducer, WorldServiceEvent, } from './types.js';
25
28
  export type { ScrubResult } from './storage.js';
26
29
  export { FsWorldStore, MemoryWorldStore, getActiveWorldStore, setActiveWorldStore, withWorldStore, hydrateInto, flushFrom, } from './world-store.js';
@@ -36,7 +39,7 @@ export { createTwinProxy } from './proxy.js';
36
39
  export type { TwinProxy, TwinProxyOptions, VendorRoute } from './proxy.js';
37
40
  export { forkTwin, isFork, readForkMeta, } from './fork.js';
38
41
  export type { ForkMeta } from './fork.js';
39
- export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from './actions.js';
42
+ export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, projectOwnerResources, OwnerStoreAmbiguousError, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from './actions.js';
40
43
  export type { ActionProjection, ProjectedDelivery, ProjectedResource, ProjectedResourcePatch, ProjectedResourceRef, TwinAction, TwinActionOp, TwinActionPrecondition, TwinActionPreconditionOp, TwinActionRevertSpec, TwinTransactionCommit, TwinTransactionCommitOp, TwinTransactionPrecondition, TwinTransactionRevertSpec, } from './actions.js';
41
44
  export { approveChangeset, assertMarkerBelongsTo, assertSafeChangesetName, assertValidVerifiers, buildChangeset, captureMarker, changesetContentHash, changesetHashMatches, changesetReadiness, CHANGESET_KIND, diffLedgers, formatApplication, formatChangeset, formatRebaseReport, narrateActions, narrationDrift, rebaseChangeset, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, MARKER_KIND, normalizeChangeset, parseVerifierExpression, replayChangeset, runChangesetVerifiers, summarizeByVendor, twinWriteShape, withApplication, withVerification, worldBootMarker, WORLD_BOOT_MARKER_ID, } from './changeset.js';
42
45
  export type { ApplyReceipt, RebaseActionResult, RebaseReport, RebaseTarget, Changeset, ChangesetAction, ChangesetApplication, CompensationEntry, ChangesetApproval, ChangesetReadiness, ChangesetVerification, ChangesetVerifier, ChangesetVerifierResult, LedgerDelta, LedgerPosition, LedgerRef, ReplayActionResult, ReplayReport, ReplayTarget, VendorSummary, WorldMarker, } from './changeset.js';
@@ -62,7 +65,7 @@ export * from './v1-removed.js';
62
65
  export { referenceField, registerReferences, packReferences, resolveReferences, type ReferenceDeclaration } from './references.js';
63
66
  export { serveHttp, nodeBuiltin, WORLD_BOOT_PATH, type HttpServer, type ServeHttpOptions, type HttpHandler } from './serve-http.js';
64
67
  export { fileResponse, contentTypeOf } from './file-response.js';
65
- export { bundleClient } from './client-bundle.js';
68
+ export { bundleClient, filePathOf } from './client-bundle.js';
66
69
  export { brandTokensResponse } from './brand-tokens.js';
67
70
  export { readResourceBlob, readResourceBlobRange, resourceBlobSize, resourceChain } from './resource-blob.js';
68
71
  export { mirrorShellUnder } from './mirror-shell.js';
package/dist/src/index.js CHANGED
@@ -14,9 +14,11 @@ export { clearRegistry, getPack, hasPack, listPacks, registerPack, pullPosture,
14
14
  export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
15
15
  export { WORLD_CLOCK_ENV, worldNow } from "./world-clock.js";
16
16
  export { WORLD_ENV_NAMES_ENV, worldEnvValue } from "./world-env.js";
17
- export { createTwinFetchFromHandler, TWIN_PREFIX_HEADER, twinPublicBase } from "./twin-fetch.js";
17
+ export { createTwinFetchFromHandler, TWIN_PREFIX_HEADER, TWIN_REQUEST_SCOPES, twinPublicBase, withRequestScopes } from "./twin-fetch.js";
18
+ export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runAsReadOnlyRequest, runAsVendorMove, writesRefused } from "./request-scope.js";
18
19
  export { compileSurface, createDerivedFetch, matchOperation } from "./derived.js";
19
- export { bindSemantics, coreFor, crossCutting, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from "./derived-core.js";
20
+ export { currentTraceparent, deliveryTraceHeaders, parseTraceparent, runWithRequestTrace, runWithTraceparent, TRACEPARENT_HEADER, traceparentForDelivery, validTraceparent } from "./trace-context.js";
21
+ export { bindSemantics, coreFor, crossCutting, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from "./derived-core.js";
20
22
  export { emitTwinEvent, eventSubscriptionMatches, listEmittable, runEmitCli } from "./emit.js";
21
23
  // The vendor-agnostic CLIENT-SIDE RATE BUDGET — the fail-closed backstop a pack's guarded client
22
24
  // routes every live vendor call through. The MECHANISM is here; the per-vendor ceiling/window/
@@ -42,7 +44,9 @@ twinLog,
42
44
  // The kernel's cross-process mutual-exclusion primitive (exclusive-create lockfile with
43
45
  // stale-holder reclaim). Public so world-runtime can guard concurrent `upWorld` claims of
44
46
  // one instance dir with the SAME lock semantics the event log uses (TWIN-36).
45
- withFileLock, worldPaths, worldStateRoot, } from "./storage.js";
47
+ withFileLock, worldPaths, worldStateRoot,
48
+ // Where another pack's store is in a World (architecture A3), and the variable the runtime names its data directory in.
49
+ ownerStoreRoots, WORLD_DATA_ENV, } from "./storage.js";
46
50
  // The pluggable persistence seam: the sync WorldStore interface, its fs (default) and
47
51
  // in-memory implementations, the active-store injection point, and the async
48
52
  // hydrate/flush boundary a serverless (Durable Object / KV / redis) entry uses.
@@ -54,7 +58,7 @@ export { FsBlobStore, MemoryBlobStore, blobDigest, getActiveBlobStore, setActive
54
58
  export { applyTwinWrite, applyTwinWriteAtomic, createTwinServer, journalTwinRequest, twinRequestCredentials, readTwinRequestJournal, resolveTwinRead, twinRequestJournalEnabled, twinRequestJournalPath, twinResources, } from "./serve.js";
55
59
  export { createTwinProxy } from "./proxy.js";
56
60
  export { forkTwin, isFork, readForkMeta, } from "./fork.js";
57
- export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from "./actions.js";
61
+ export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, projectOwnerResources, OwnerStoreAmbiguousError, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from "./actions.js";
58
62
  // ── Operator control plane (R19/R20): remote refs, queue lifecycle, push ledger,
59
63
  // apply leases, plan, status. (the twins architecture notes)
60
64
  // The CHANGESET primitive + the ledger DIFF (docs/concepts/the-model.md v0) — "commit" and
@@ -87,7 +91,7 @@ export { referenceField, registerReferences, packReferences, resolveReferences }
87
91
  export { serveHttp, nodeBuiltin, WORLD_BOOT_PATH } from "./serve-http.js";
88
92
  // two runtime-neutral helpers for a pack's serve path: a file as a Response, a mirror's client bundle
89
93
  export { fileResponse, contentTypeOf } from "./file-response.js";
90
- export { bundleClient } from "./client-bundle.js";
94
+ export { bundleClient, filePathOf } from "./client-bundle.js";
91
95
  // the brand's tokens and faces for a Volter page (the console, the site, the UI kit), fetched at build
92
96
  export { brandTokensResponse } from "./brand-tokens.js";
93
97
  export { readResourceBlob, readResourceBlobRange, resourceBlobSize, resourceChain } from "./resource-blob.js";
package/dist/src/log.d.ts CHANGED
@@ -64,6 +64,8 @@ export type Entry = {
64
64
  /** a landed copy under the vendor's id: the local subject id it stands for */
65
65
  aliasOf?: string;
66
66
  receipt?: Receipt;
67
+ /** the W3C traceparent of the request that wrote this entry (trace-context.ts); never part of its identity */
68
+ traceparent?: string;
67
69
  /** a v1 observed row kept verbatim for `listEvents`; folds nothing unless `fields` was derived */
68
70
  event?: WorldServiceEvent;
69
71
  [extra: string]: unknown;
@@ -5,12 +5,14 @@ import type { TwinEmitter } from './emit.js';
5
5
  import { type StateSystemAdapters } from './state-system.js';
6
6
  /**
7
7
  * How a pack's clients ADDRESS it. The first three are HTTP API styles; `raw-tcp` is the
8
- * RAW-PROTOCOL class — a pack whose clients speak a line protocol directly over a TCP socket
9
- * rather than HTTP, so none of the HTTP machinery applies: no `browserRouting`, and no entry in
10
- * the injector's `VENDOR_HOSTS` host map is possible (the injector patches http/fetch and never
11
- * sees the traffic). Such a pack is wired into a world through app-read host/port env instead,
12
- * and this value is what tells a reader that "no injector entry" is STRUCTURAL rather than a
13
- * missing wiring point. `packages/twin/smtp` is the first.
8
+ * RAW-PROTOCOL class — a pack whose clients speak their protocol directly over a TCP socket
9
+ * rather than through the HTTP/1 fetch machinery, so none of the HTTP machinery applies: no
10
+ * `browserRouting`, and no entry in the injector's `VENDOR_HOSTS` host map is possible (the
11
+ * injector patches http/fetch and never sees the traffic). Such a pack is wired into a world
12
+ * through app-read host/port env instead, and this value is what tells a reader that "no injector
13
+ * entry" is STRUCTURAL rather than a missing wiring point. `packages/twin/smtp` (a line protocol)
14
+ * is the first; `packages/twin/temporal` (gRPC: HTTP/2 framing its clients drive themselves) the
15
+ * second.
14
16
  *
15
17
  * Deliberately the transport CLASS, not the protocol name: naming the wire protocol would put a
16
18
  * vendor id in the kernel the moment a pack is named after its protocol, which is exactly what