@volter/world-core 2.0.1 → 2.0.3
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 +36 -0
- package/dist/src/actions.d.ts +3 -0
- package/dist/src/actions.js +13 -2
- package/dist/src/blob-store.d.ts +3 -0
- package/dist/src/blob-store.js +15 -1
- package/dist/src/client-bundle.d.ts +4 -0
- package/dist/src/client-bundle.js +13 -2
- package/dist/src/derived-core.d.ts +6 -0
- package/dist/src/derived-core.js +17 -3
- package/dist/src/derived.js +5 -2
- package/dist/src/git/refs.js +5 -3
- package/dist/src/head.js +12 -2
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.js +5 -3
- package/dist/src/log.d.ts +2 -0
- package/dist/src/request-scope.d.ts +28 -0
- package/dist/src/request-scope.js +70 -0
- package/dist/src/storage.js +2 -0
- package/dist/src/trace-context.d.ts +31 -0
- package/dist/src/trace-context.js +78 -0
- package/dist/src/twin-fetch.d.ts +16 -0
- package/dist/src/twin-fetch.js +66 -3
- package/dist/src/world-store.js +9 -0
- package/dist/vendor-hosts.cjs +6 -5
- package/generated/pack-facts.json +36 -0
- package/package.json +1 -1
- package/src/actions.ts +16 -2
- package/src/blob-store.ts +15 -1
- package/src/client-bundle.ts +15 -2
- package/src/derived-core.ts +17 -3
- package/src/derived.ts +5 -2
- package/src/git/refs.ts +5 -3
- package/src/head.ts +11 -2
- package/src/index.ts +6 -3
- package/src/log.ts +2 -0
- package/src/request-scope.ts +74 -0
- package/src/storage.ts +2 -0
- package/src/trace-context.ts +87 -0
- package/src/twin-fetch.ts +69 -3
- package/src/world-store.ts +9 -0
- package/vendor-hosts.cjs +6 -5
|
@@ -506,6 +506,10 @@
|
|
|
506
506
|
{
|
|
507
507
|
"host": "cdn.bsky.app",
|
|
508
508
|
"pathPattern": "^/img/"
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
"host": "bsky.app",
|
|
512
|
+
"pathPattern": "^/profile/[^/]+/?$|^/profile/[^/]+/post/[A-Za-z0-9._:~-]{1,512}/?$"
|
|
509
513
|
}
|
|
510
514
|
],
|
|
511
515
|
"archetype": "crud",
|
|
@@ -4274,6 +4278,38 @@
|
|
|
4274
4278
|
{
|
|
4275
4279
|
"host": "api.twitter.com",
|
|
4276
4280
|
"pathPattern": "^(?:/2/media/upload/?|/2/media/upload/initialize/?|/2/media/upload/[0-9]+/(?:append|finalize)/?|/2/tweets/?|/2/tweets/[^/]+/?|/2/tweets/search/recent/?|/2/users/(?!me/?$)[^/]+/?|/2/users/by/username/[^/]+/?|/2/users/[^/]+/mentions/?|/2/users/[^/]+/tweets/?|/2/users/[^/]+/timelines/reverse_chronological/?)$|^/_twin/(?!assets/consent\\.(?:js|css)$)"
|
|
4281
|
+
},
|
|
4282
|
+
{
|
|
4283
|
+
"host": "x.com",
|
|
4284
|
+
"pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
|
|
4285
|
+
},
|
|
4286
|
+
{
|
|
4287
|
+
"host": "twitter.com",
|
|
4288
|
+
"pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
|
|
4289
|
+
},
|
|
4290
|
+
{
|
|
4291
|
+
"host": "www.x.com",
|
|
4292
|
+
"pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
|
|
4293
|
+
},
|
|
4294
|
+
{
|
|
4295
|
+
"host": "www.twitter.com",
|
|
4296
|
+
"pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
|
|
4297
|
+
},
|
|
4298
|
+
{
|
|
4299
|
+
"host": "mobile.twitter.com",
|
|
4300
|
+
"pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
|
|
4301
|
+
},
|
|
4302
|
+
{
|
|
4303
|
+
"host": "mobile.x.com",
|
|
4304
|
+
"pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
|
|
4305
|
+
},
|
|
4306
|
+
{
|
|
4307
|
+
"host": "pbs.twimg.com",
|
|
4308
|
+
"pathPattern": "^/media/[0-9a-f]{64}\\.(?:jpg|png|gif|webp)$|^/ext_tw_video_thumb/[0-9]{1,19}/pu/img/[0-9a-f]{64}\\.svg$"
|
|
4309
|
+
},
|
|
4310
|
+
{
|
|
4311
|
+
"host": "video.twimg.com",
|
|
4312
|
+
"pathPattern": "^/ext_tw_video/[0-9]{1,19}/pu/vid/avc1/[0-9]+x[0-9]+/[0-9a-f]{64}\\.mp4$"
|
|
4277
4313
|
}
|
|
4278
4314
|
],
|
|
4279
4315
|
"archetype": "crud",
|
package/dist/src/actions.d.ts
CHANGED
|
@@ -87,6 +87,9 @@ export type TwinAction = {
|
|
|
87
87
|
* alone, with no dependence on actionId/pushId naming conventions.
|
|
88
88
|
*/
|
|
89
89
|
correlationId?: string;
|
|
90
|
+
/** The W3C `traceparent` of the request that caused this action, when it carried a valid one
|
|
91
|
+
* (trace-context.ts). Authoring metadata like correlationId: excluded from replay identity. */
|
|
92
|
+
traceparent?: string;
|
|
90
93
|
};
|
|
91
94
|
export type TwinTransactionCommit = TwinAction;
|
|
92
95
|
export type TwinTransactionCommitOp = TwinActionOp;
|
package/dist/src/actions.js
CHANGED
|
@@ -19,7 +19,9 @@ import { canonicalJson, subjectKey } from "./hash.js";
|
|
|
19
19
|
import { appendDurable, eventsLockPath, ownerStoreRoots, projectionLockPath, twinLog, withFileLock, withoutIdentityClaim, worldPaths } from "./storage.js";
|
|
20
20
|
import { landAsPlaceholder, placeholderPullActive } from "./placeholder-remote.js";
|
|
21
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
|
-
|
|
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
|
}
|
|
@@ -401,6 +410,7 @@ export function resolveSubjectId(service, type, id, root) {
|
|
|
401
410
|
* the change is counted exactly once. Returns the observed event id.
|
|
402
411
|
*/
|
|
403
412
|
export function confirmAction(opts) {
|
|
413
|
+
refuseReadOnlyWrite(opts.service);
|
|
404
414
|
// LANDING (log.ts): the entry is copied to the parent log with its receipt — no confirm row, no
|
|
405
415
|
// suppression; the fold skips a branch entry the parent holds.
|
|
406
416
|
const paths = worldPaths(opts.service, opts.root);
|
|
@@ -431,6 +441,7 @@ export function confirmAction(opts) {
|
|
|
431
441
|
* Only `set` rows are revertable — reverting a revert or a confirm is a category error.
|
|
432
442
|
*/
|
|
433
443
|
export function revertAction(opts) {
|
|
444
|
+
refuseReadOnlyWrite(opts.service);
|
|
434
445
|
return withFileLock(projectionLock(opts.service, opts.root), () => withFileLock(actionsLock(opts.service, opts.root), () => {
|
|
435
446
|
const all = listActions(opts.service, opts.root);
|
|
436
447
|
const target = all.find((a) => a.id === opts.actionId);
|
package/dist/src/blob-store.d.ts
CHANGED
|
@@ -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>;
|
package/dist/src/blob-store.js
CHANGED
|
@@ -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>;
|
package/dist/src/derived-core.js
CHANGED
|
@@ -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
|
-
|
|
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 = {}) {
|
package/dist/src/derived.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/src/git/refs.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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. */
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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';
|
|
@@ -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 {
|
|
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/
|
|
@@ -89,7 +91,7 @@ export { referenceField, registerReferences, packReferences, resolveReferences }
|
|
|
89
91
|
export { serveHttp, nodeBuiltin, WORLD_BOOT_PATH } from "./serve-http.js";
|
|
90
92
|
// two runtime-neutral helpers for a pack's serve path: a file as a Response, a mirror's client bundle
|
|
91
93
|
export { fileResponse, contentTypeOf } from "./file-response.js";
|
|
92
|
-
export { bundleClient } from "./client-bundle.js";
|
|
94
|
+
export { bundleClient, filePathOf } from "./client-bundle.js";
|
|
93
95
|
// the brand's tokens and faces for a Volter page (the console, the site, the UI kit), fetched at build
|
|
94
96
|
export { brandTokensResponse } from "./brand-tokens.js";
|
|
95
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;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The request header that marks a request read-only. Its one value is `1`. */
|
|
2
|
+
export declare const READ_ONLY_REQUEST_HEADER = "x-volter-read-only";
|
|
3
|
+
/** Whether a request carries the read-only marker. */
|
|
4
|
+
export declare function isReadOnlyRequest(request: Request | {
|
|
5
|
+
headers: Headers;
|
|
6
|
+
}): boolean;
|
|
7
|
+
/** A write a read-only request attempted: refused before anything was written (rule D3's 405). */
|
|
8
|
+
export declare class ReadOnlyRequestError extends Error {
|
|
9
|
+
readonly service: string;
|
|
10
|
+
constructor(service: string);
|
|
11
|
+
}
|
|
12
|
+
/** Run `fn` as a read-only request: every write it attempts outside a vendor move is refused. The
|
|
13
|
+
* answer says whether one was — also when the handler caught the refusal and answered something
|
|
14
|
+
* else, so a caller can answer the refusal whatever the handler made of it. */
|
|
15
|
+
export declare function runAsReadOnlyRequest<T>(fn: () => T | Promise<T>): Promise<{
|
|
16
|
+
value: T;
|
|
17
|
+
refused: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
refused: ReadOnlyRequestError;
|
|
20
|
+
}>;
|
|
21
|
+
/** Run `fn` as the VENDOR's own move (a pack's catch-up): its writes land even under a read-only
|
|
22
|
+
* request. Outside one it changes nothing. */
|
|
23
|
+
export declare function runAsVendorMove<T>(fn: () => T): T;
|
|
24
|
+
/** Whether a write attempted here would be refused. */
|
|
25
|
+
export declare function writesRefused(): boolean;
|
|
26
|
+
/** The write seam's check: throws (and records) the refusal when a write attempted here is a
|
|
27
|
+
* read-only request's. Called by every appender before it writes anything. */
|
|
28
|
+
export declare function refuseReadOnlyWrite(service: string): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// THE READ-ONLY REQUEST (docs/contributing/architecture.md, "Viewing a World"): a request that
|
|
2
|
+
// carries `x-volter-read-only: 1` is served with the twin's writes refused — whatever operation it
|
|
3
|
+
// names and whatever HTTP method it came by (an RPC wire POSTs its reads). The marker only ever
|
|
4
|
+
// RESTRICTS, so the twin trusts it without auth: a caller who sends it only limits itself. The
|
|
5
|
+
// World's doors set it on every read-scope request and never classify operations themselves.
|
|
6
|
+
//
|
|
7
|
+
// Enforcement lives at the kernel's write seam: every appender (actions.ts, storage.ts appendEvent)
|
|
8
|
+
// asks `refuseReadOnlyWrite` before it writes anything, so the first write a read-only request
|
|
9
|
+
// attempts is refused and nothing is partially applied. The VENDOR's own moves are not the caller's
|
|
10
|
+
// writes: a pack's catch-up (a renewal, a payout, a file's expiry falling due by the World clock)
|
|
11
|
+
// runs under `runAsVendorMove` and still lands under a read-only request, as a real vendor renews a
|
|
12
|
+
// subscription whoever is looking.
|
|
13
|
+
//
|
|
14
|
+
// Created on first use, never at import: a browser bundle of a mirror client carries the kernel and
|
|
15
|
+
// has no AsyncLocalStorage (see serve.ts identitySlot).
|
|
16
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
17
|
+
/** The request header that marks a request read-only. Its one value is `1`. */
|
|
18
|
+
export const READ_ONLY_REQUEST_HEADER = 'x-volter-read-only';
|
|
19
|
+
/** Whether a request carries the read-only marker. */
|
|
20
|
+
export function isReadOnlyRequest(request) {
|
|
21
|
+
return request.headers.get(READ_ONLY_REQUEST_HEADER)?.trim() === '1';
|
|
22
|
+
}
|
|
23
|
+
/** A write a read-only request attempted: refused before anything was written (rule D3's 405). */
|
|
24
|
+
export class ReadOnlyRequestError extends Error {
|
|
25
|
+
service;
|
|
26
|
+
constructor(service) {
|
|
27
|
+
super(`${service}: this request is read-only (${READ_ONLY_REQUEST_HEADER}: 1); it cannot write`);
|
|
28
|
+
this.service = service;
|
|
29
|
+
this.name = 'ReadOnlyRequestError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let writeScopeStore;
|
|
33
|
+
const writeScope = () => (writeScopeStore ??= new AsyncLocalStorage());
|
|
34
|
+
/** Run `fn` as a read-only request: every write it attempts outside a vendor move is refused. The
|
|
35
|
+
* answer says whether one was — also when the handler caught the refusal and answered something
|
|
36
|
+
* else, so a caller can answer the refusal whatever the handler made of it. */
|
|
37
|
+
export async function runAsReadOnlyRequest(fn) {
|
|
38
|
+
const scope = { readOnly: true, vendorMove: false, refusals: [] };
|
|
39
|
+
try {
|
|
40
|
+
const value = await writeScope().run(scope, fn);
|
|
41
|
+
const refused = scope.refusals[0];
|
|
42
|
+
return refused ? { refused } : { value, refused: undefined };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const refused = scope.refusals[0] ?? (error instanceof ReadOnlyRequestError ? error : undefined);
|
|
46
|
+
if (refused)
|
|
47
|
+
return { refused };
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Run `fn` as the VENDOR's own move (a pack's catch-up): its writes land even under a read-only
|
|
52
|
+
* request. Outside one it changes nothing. */
|
|
53
|
+
export function runAsVendorMove(fn) {
|
|
54
|
+
const current = writeScope().getStore();
|
|
55
|
+
return current ? writeScope().run({ ...current, vendorMove: true }, fn) : fn();
|
|
56
|
+
}
|
|
57
|
+
/** Whether a write attempted here would be refused. */
|
|
58
|
+
export function writesRefused() {
|
|
59
|
+
const scope = writeScopeStore?.getStore();
|
|
60
|
+
return scope !== undefined && scope.readOnly && !scope.vendorMove;
|
|
61
|
+
}
|
|
62
|
+
/** The write seam's check: throws (and records) the refusal when a write attempted here is a
|
|
63
|
+
* read-only request's. Called by every appender before it writes anything. */
|
|
64
|
+
export function refuseReadOnlyWrite(service) {
|
|
65
|
+
if (!writesRefused())
|
|
66
|
+
return;
|
|
67
|
+
const error = new ReadOnlyRequestError(service);
|
|
68
|
+
writeScopeStore.getStore().refusals.push(error);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
package/dist/src/storage.js
CHANGED
|
@@ -3,6 +3,7 @@ import { assertNotBeingRemoved, withAncestryLock, withStateRemoval } from "./anc
|
|
|
3
3
|
import { appendParentEntry, toEntry } from "./log.js";
|
|
4
4
|
import { GenericWorldStateSchema, WorldServiceEventSchema, } from "./schemas.js";
|
|
5
5
|
import { getActiveWorldStore } from "./world-store.js";
|
|
6
|
+
import { refuseReadOnlyWrite } from "./request-scope.js";
|
|
6
7
|
import { parentEntries, toEvent } from "./log.js";
|
|
7
8
|
function nowIso() {
|
|
8
9
|
return new Date().toISOString();
|
|
@@ -226,6 +227,7 @@ export function projectionLockPath(paths) {
|
|
|
226
227
|
*/
|
|
227
228
|
export function appendEvent(event, root) {
|
|
228
229
|
const parsed = WorldServiceEventSchema.parse(event);
|
|
230
|
+
refuseReadOnlyWrite(parsed.service); // a read-only request writes nothing (request-scope.ts)
|
|
229
231
|
const paths = worldPaths(parsed.service, root);
|
|
230
232
|
ensureEventDirs(paths);
|
|
231
233
|
const { appended } = appendParentEntry(toEntry(parsed), root);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const TRACEPARENT_HEADER = "traceparent";
|
|
2
|
+
export type Traceparent = {
|
|
3
|
+
version: string;
|
|
4
|
+
traceId: string;
|
|
5
|
+
parentId: string;
|
|
6
|
+
flags: string;
|
|
7
|
+
};
|
|
8
|
+
/** A `traceparent` header value parsed strictly, or null: malformed, version `ff`, or an all-zero
|
|
9
|
+
* trace-id or parent-id is no trace at all (the spec: such a header is ignored). */
|
|
10
|
+
export declare function parseTraceparent(value: unknown): Traceparent | null;
|
|
11
|
+
/** The value itself when it is a valid traceparent (normalized: trimmed), else undefined. */
|
|
12
|
+
export declare function validTraceparent(value: unknown): string | undefined;
|
|
13
|
+
/** Run `fn` with `traceparent` as the request's trace context; an invalid value runs `fn` outside any. */
|
|
14
|
+
export declare function runWithTraceparent<T>(traceparent: string | undefined, fn: () => T): T;
|
|
15
|
+
/** The trace context of the request being handled, when it carried a valid traceparent. */
|
|
16
|
+
export declare function currentTraceparent(): string | undefined;
|
|
17
|
+
/** Run `fn` inside the trace context `request` carries (its `traceparent` header), if any. */
|
|
18
|
+
export declare function runWithRequestTrace<T>(request: Request, fn: () => T): T;
|
|
19
|
+
/**
|
|
20
|
+
* The `traceparent` an outbound delivery carries: a CHILD of its cause — the same trace-id and
|
|
21
|
+
* flags, a new parent-id — so the receiving handler continues the cause's trace. The cause is the
|
|
22
|
+
* given traceparent, or an entry's recorded one, or (omitted) the trace context of the request
|
|
23
|
+
* being handled. No valid cause, no traceparent: a delivery never invents a trace.
|
|
24
|
+
*/
|
|
25
|
+
export declare function traceparentForDelivery(cause?: string | {
|
|
26
|
+
traceparent?: unknown;
|
|
27
|
+
} | null): string | undefined;
|
|
28
|
+
/** `traceparentForDelivery` as headers to spread into a delivery's own: `{ traceparent }` or `{}`. */
|
|
29
|
+
export declare function deliveryTraceHeaders(cause?: string | {
|
|
30
|
+
traceparent?: unknown;
|
|
31
|
+
} | null): Record<string, string>;
|