@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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// W3C TRACE CONTEXT across a World (https://www.w3.org/TR/trace-context/): one cause followed across
|
|
2
|
+
// vendors by the tracing standard, never an id of our own. An app instrumented with OpenTelemetry
|
|
3
|
+
// sends `traceparent` on its outgoing calls; a real vendor ignores it, so the vendor wire is
|
|
4
|
+
// unchanged. The kernel serve seams (twin-fetch.ts, derived.ts) run a handler inside the request's
|
|
5
|
+
// valid traceparent; every entry appended while handling it records that value (actions.ts
|
|
6
|
+
// `traceparent`, authoring metadata excluded from replay identity like `correlationId`); and a
|
|
7
|
+
// pack's outbound delivery the write caused (a webhook to the app) carries a CHILD of it — the
|
|
8
|
+
// same trace-id, a new parent-id — so the app's handler continues the same trace.
|
|
9
|
+
//
|
|
10
|
+
// Nothing here is vendor knowledge, and nothing runs at import: the async-context store is made on
|
|
11
|
+
// first use (a browser bundle of a mirror client carries the kernel and has no AsyncLocalStorage).
|
|
12
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
13
|
+
export const TRACEPARENT_HEADER = 'traceparent';
|
|
14
|
+
// version-traceid-parentid-flags, lowercase hex only (the spec's HEXDIGLC). Version ff is invalid;
|
|
15
|
+
// a future version is read by its version-00 prefix only when nothing else follows, which keeps the
|
|
16
|
+
// accepted form exactly the one this module emits.
|
|
17
|
+
const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
18
|
+
const ZERO_TRACE = '0'.repeat(32);
|
|
19
|
+
const ZERO_SPAN = '0'.repeat(16);
|
|
20
|
+
/** A `traceparent` header value parsed strictly, or null: malformed, version `ff`, or an all-zero
|
|
21
|
+
* trace-id or parent-id is no trace at all (the spec: such a header is ignored). */
|
|
22
|
+
export function parseTraceparent(value) {
|
|
23
|
+
if (typeof value !== 'string')
|
|
24
|
+
return null;
|
|
25
|
+
const m = TRACEPARENT.exec(value.trim());
|
|
26
|
+
if (!m)
|
|
27
|
+
return null;
|
|
28
|
+
const [, version, traceId, parentId, flags] = m;
|
|
29
|
+
if (version === 'ff' || traceId === ZERO_TRACE || parentId === ZERO_SPAN)
|
|
30
|
+
return null;
|
|
31
|
+
return { version, traceId, parentId, flags };
|
|
32
|
+
}
|
|
33
|
+
/** The value itself when it is a valid traceparent (normalized: trimmed), else undefined. */
|
|
34
|
+
export function validTraceparent(value) {
|
|
35
|
+
const parsed = parseTraceparent(value);
|
|
36
|
+
return parsed ? `${parsed.version}-${parsed.traceId}-${parsed.parentId}-${parsed.flags}` : undefined;
|
|
37
|
+
}
|
|
38
|
+
// created on first use, never at import (see the header)
|
|
39
|
+
let traceStore;
|
|
40
|
+
const traceScope = () => (traceStore ??= new AsyncLocalStorage());
|
|
41
|
+
/** Run `fn` with `traceparent` as the request's trace context; an invalid value runs `fn` outside any. */
|
|
42
|
+
export function runWithTraceparent(traceparent, fn) {
|
|
43
|
+
const valid = validTraceparent(traceparent);
|
|
44
|
+
return valid ? traceScope().run(valid, fn) : fn();
|
|
45
|
+
}
|
|
46
|
+
/** The trace context of the request being handled, when it carried a valid traceparent. */
|
|
47
|
+
export function currentTraceparent() {
|
|
48
|
+
return traceStore?.getStore();
|
|
49
|
+
}
|
|
50
|
+
/** Run `fn` inside the trace context `request` carries (its `traceparent` header), if any. */
|
|
51
|
+
export function runWithRequestTrace(request, fn) {
|
|
52
|
+
return runWithTraceparent(request.headers.get(TRACEPARENT_HEADER) ?? undefined, fn);
|
|
53
|
+
}
|
|
54
|
+
function newSpanId() {
|
|
55
|
+
const bytes = new Uint8Array(8);
|
|
56
|
+
for (;;) {
|
|
57
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
58
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
59
|
+
if (hex !== ZERO_SPAN)
|
|
60
|
+
return hex;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The `traceparent` an outbound delivery carries: a CHILD of its cause — the same trace-id and
|
|
65
|
+
* flags, a new parent-id — so the receiving handler continues the cause's trace. The cause is the
|
|
66
|
+
* given traceparent, or an entry's recorded one, or (omitted) the trace context of the request
|
|
67
|
+
* being handled. No valid cause, no traceparent: a delivery never invents a trace.
|
|
68
|
+
*/
|
|
69
|
+
export function traceparentForDelivery(cause) {
|
|
70
|
+
const source = cause === undefined ? currentTraceparent() : typeof cause === 'string' ? cause : cause?.traceparent;
|
|
71
|
+
const parent = parseTraceparent(source);
|
|
72
|
+
return parent ? `00-${parent.traceId}-${newSpanId()}-${parent.flags}` : undefined;
|
|
73
|
+
}
|
|
74
|
+
/** `traceparentForDelivery` as headers to spread into a delivery's own: `{ traceparent }` or `{}`. */
|
|
75
|
+
export function deliveryTraceHeaders(cause) {
|
|
76
|
+
const traceparent = traceparentForDelivery(cause);
|
|
77
|
+
return traceparent ? { [TRACEPARENT_HEADER]: traceparent } : {};
|
|
78
|
+
}
|
package/dist/src/twin-fetch.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReadOnlyRequestError } from './request-scope.js';
|
|
1
2
|
/** The header a host sets when it mounts a twin under a path (a served World's wire:
|
|
2
3
|
* `/<org>/<world>/<vendor>`), the reverse-proxy convention; with it the host sets
|
|
3
4
|
* `x-forwarded-host` and `x-forwarded-proto` to where the World is reached. */
|
|
@@ -21,6 +22,21 @@ export type TwinStreamConnection = {
|
|
|
21
22
|
close(): void;
|
|
22
23
|
};
|
|
23
24
|
export type TwinStream = (sink: TwinStreamSink, peer: string) => TwinStreamConnection;
|
|
25
|
+
/** The request scopes a pack wrapped in `withRequestScopes` enforces, advertised on its `GET /twin` as
|
|
26
|
+
* `requestScopes`: `read` — a request carrying `x-volter-read-only: 1` (request-scope.ts) has every
|
|
27
|
+
* write it attempts refused at the kernel's write seam, whatever operation it names. A World's doors
|
|
28
|
+
* forward a read-scope request that is not a GET only to a twin that advertises it. */
|
|
29
|
+
export declare const TWIN_REQUEST_SCOPES: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* THE READ SCOPE for a pack that writes its own fetch (the derived packs, the byte-wire packs): a
|
|
32
|
+
* request carrying the read-only marker runs with the kernel's write seam refusing its writes, and a
|
|
33
|
+
* refused write answers `refuse` (the vendor's own read-only error; a generic 405 without one) —
|
|
34
|
+
* whatever the handler made of the refusal. `GET /twin` advertises `requestScopes`. Everything else
|
|
35
|
+
* passes through untouched; the wrapped fetch keeps its own properties (a derived fetch's `owners`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function withRequestScopes<F extends (request: Request) => Promise<Response>>(fetch: F, opts?: {
|
|
38
|
+
refuse?: (request: Request, error: ReadOnlyRequestError) => Response | Promise<Response>;
|
|
39
|
+
}): F;
|
|
24
40
|
export type TwinFetchHandlerResult = {
|
|
25
41
|
status: number;
|
|
26
42
|
body: unknown;
|
package/dist/src/twin-fetch.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// Workerd-clean by construction: no Bun APIs, no fs, no clock but worldNow() — the same
|
|
11
11
|
// closure serves under Bun.serve locally and mounted in-process on Cloudflare.
|
|
12
12
|
import { runWithCorrelationId } from "./actions.js";
|
|
13
|
+
import { runWithRequestTrace } from "./trace-context.js";
|
|
14
|
+
import { isReadOnlyRequest, runAsReadOnlyRequest } from "./request-scope.js";
|
|
13
15
|
import { worldNow } from "./world-clock.js";
|
|
14
16
|
/** The header a host sets when it mounts a twin under a path (a served World's wire:
|
|
15
17
|
* `/<org>/<world>/<vendor>`), the reverse-proxy convention; with it the host sets
|
|
@@ -29,6 +31,50 @@ export function twinPublicBase(request) {
|
|
|
29
31
|
const origin = host && /^[A-Za-z0-9.-]+(:\d+)?$/.test(host) && (proto === 'http' || proto === 'https') ? `${proto}://${host}` : new URL(request.url).origin;
|
|
30
32
|
return `${origin}${prefix.replace(/\/+$/, '')}`;
|
|
31
33
|
}
|
|
34
|
+
/** The request scopes a pack wrapped in `withRequestScopes` enforces, advertised on its `GET /twin` as
|
|
35
|
+
* `requestScopes`: `read` — a request carrying `x-volter-read-only: 1` (request-scope.ts) has every
|
|
36
|
+
* write it attempts refused at the kernel's write seam, whatever operation it names. A World's doors
|
|
37
|
+
* forward a read-scope request that is not a GET only to a twin that advertises it. */
|
|
38
|
+
export const TWIN_REQUEST_SCOPES = ['read'];
|
|
39
|
+
/** `GET /twin`'s body with the request scopes this seam enforces. */
|
|
40
|
+
function advertised(manifest) {
|
|
41
|
+
return manifest !== null && typeof manifest === 'object' && !Array.isArray(manifest) ? { ...manifest, requestScopes: [...TWIN_REQUEST_SCOPES] } : manifest;
|
|
42
|
+
}
|
|
43
|
+
/** The answer to a read-only request's write when the twin names no vendor-shaped one (rule D3). */
|
|
44
|
+
function readOnlyRefusal(error) {
|
|
45
|
+
return Response.json({ error: 'read_only', message: error.message }, { status: 405 });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* THE READ SCOPE for a pack that writes its own fetch (the derived packs, the byte-wire packs): a
|
|
49
|
+
* request carrying the read-only marker runs with the kernel's write seam refusing its writes, and a
|
|
50
|
+
* refused write answers `refuse` (the vendor's own read-only error; a generic 405 without one) —
|
|
51
|
+
* whatever the handler made of the refusal. `GET /twin` advertises `requestScopes`. Everything else
|
|
52
|
+
* passes through untouched; the wrapped fetch keeps its own properties (a derived fetch's `owners`).
|
|
53
|
+
*/
|
|
54
|
+
export function withRequestScopes(fetch, opts = {}) {
|
|
55
|
+
const scoped = async (request) => {
|
|
56
|
+
const url = new URL(request.url);
|
|
57
|
+
if (request.method === 'GET' && (url.pathname.replace(/\/+$/, '') || '/') === '/twin') {
|
|
58
|
+
const answer = await fetch(request);
|
|
59
|
+
if (!answer.ok || !(answer.headers.get('content-type') ?? '').includes('json'))
|
|
60
|
+
return answer;
|
|
61
|
+
const headers = new Headers(answer.headers);
|
|
62
|
+
headers.delete('content-length');
|
|
63
|
+
return new Response(JSON.stringify(advertised(await answer.json())), { status: answer.status, headers });
|
|
64
|
+
}
|
|
65
|
+
// the request's W3C traceparent follows every entry the fetch writes, as through the kernel's own adapters
|
|
66
|
+
// (a custom fetch never entered it, so the timeline's trace filter missed its entries)
|
|
67
|
+
return runWithRequestTrace(request, async () => {
|
|
68
|
+
if (!isReadOnlyRequest(request))
|
|
69
|
+
return fetch(request);
|
|
70
|
+
const out = await runAsReadOnlyRequest(() => fetch(request));
|
|
71
|
+
if (!out.refused)
|
|
72
|
+
return out.value;
|
|
73
|
+
return opts.refuse ? await opts.refuse(request, out.refused) : readOnlyRefusal(out.refused);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
return Object.assign(scoped, fetch);
|
|
77
|
+
}
|
|
32
78
|
export function createTwinFetchFromHandler(handler, config) {
|
|
33
79
|
const readOnly = config.readOnly ?? false;
|
|
34
80
|
return async function twinFetch(request) {
|
|
@@ -63,18 +109,23 @@ export function createTwinFetchFromHandler(handler, config) {
|
|
|
63
109
|
// world instant. A client-settable occurredAt would be a direct R9 hole.
|
|
64
110
|
// D3 — the wire's request id becomes the correlation of every action this handler appends.
|
|
65
111
|
const requestId = request.headers.get('x-twins-request-id') ?? undefined;
|
|
66
|
-
const invoke = () => handler({
|
|
112
|
+
const invoke = (asReadOnly = readOnly) => handler({
|
|
67
113
|
...config.handlerOptions,
|
|
68
114
|
...config.extras?.(request, url),
|
|
69
115
|
method: request.method,
|
|
70
116
|
path: url.pathname + (url.search || ''),
|
|
71
117
|
body,
|
|
72
118
|
headers,
|
|
73
|
-
readOnly,
|
|
119
|
+
readOnly: asReadOnly,
|
|
74
120
|
occurredAt: worldNow(),
|
|
75
121
|
...(config.root !== undefined ? { root: config.root } : {}),
|
|
76
122
|
});
|
|
77
|
-
|
|
123
|
+
// a read-only request is, to the handler, a request to a read-only twin: the pack refuses its
|
|
124
|
+
// writes in the vendor's own shape (D3), and the kernel's write seam refuses any it misses
|
|
125
|
+
const scoped = readOnly || isReadOnlyRequest(request);
|
|
126
|
+
const run = (asReadOnly = scoped) => (requestId ? runWithCorrelationId(requestId, () => invoke(asReadOnly)) : invoke(asReadOnly));
|
|
127
|
+
// W3C trace context: a valid incoming traceparent scopes the handler, so its entries record it
|
|
128
|
+
const result = await runWithRequestTrace(request, () => (isReadOnlyRequest(request) ? readScoped(run) : run()));
|
|
78
129
|
// A null/undefined body is an EMPTY reply (`JSON.stringify(null)` would serve the
|
|
79
130
|
// four bytes "null") — and so is the estate's own 204 idiom, `body: ''` with a
|
|
80
131
|
// null-body status: workerd THROWS on any body with 204/205/304 (review B2), where
|
|
@@ -89,3 +140,15 @@ export function createTwinFetchFromHandler(handler, config) {
|
|
|
89
140
|
});
|
|
90
141
|
};
|
|
91
142
|
}
|
|
143
|
+
/** A read-only request through the handler seam: the handler runs once, with its writes refused at
|
|
144
|
+
* the kernel's write seam; a write it attempted answers 405 (rule D3). The handler is never asked
|
|
145
|
+
* twice (a second run would repeat whatever it did before its first write: a rate-limit slot, a
|
|
146
|
+
* vendor call). The seam enforces the marker for whoever sends it, but does not advertise
|
|
147
|
+
* `requestScopes`: a pack opts in to being handed a World's read-token requests by wrapping its
|
|
148
|
+
* fetch in `withRequestScopes`, once it has made sure nothing it does on a read escapes the seam. */
|
|
149
|
+
async function readScoped(run) {
|
|
150
|
+
const out = await runAsReadOnlyRequest(() => run());
|
|
151
|
+
if (!out.refused)
|
|
152
|
+
return out.value;
|
|
153
|
+
return { status: 405, body: { error: 'read_only', message: out.refused.message } };
|
|
154
|
+
}
|
package/dist/src/world-store.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// `node:fs`. See its module header.
|
|
26
26
|
import { appendFileSync, chmodSync, closeSync, existsSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
27
27
|
import { hostname } from 'node:os';
|
|
28
|
+
import { refuseReadOnlyWrite } from "./request-scope.js";
|
|
28
29
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
29
30
|
// ── FsWorldStore — the DEFAULT, byte-identical to the historical node:fs behavior ────
|
|
30
31
|
let fsAtomicSequence = 0;
|
|
@@ -122,6 +123,7 @@ export class FsWorldStore {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
append(path, data) {
|
|
126
|
+
refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
|
|
125
127
|
// Historical appendDurable: a completed append syscall survives a process crash;
|
|
126
128
|
// fsync additionally survives kernel-panic/power-loss when VOLTER_DURABLE=1.
|
|
127
129
|
const fd = openSync(path, 'a');
|
|
@@ -135,6 +137,7 @@ export class FsWorldStore {
|
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
write(path, data, options = {}) {
|
|
140
|
+
refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
|
|
138
141
|
if (!options.secret) {
|
|
139
142
|
mkdirSync(dirname(path), { recursive: true });
|
|
140
143
|
writeFileSync(path, data);
|
|
@@ -145,6 +148,7 @@ export class FsWorldStore {
|
|
|
145
148
|
chmodSync(path, 0o600);
|
|
146
149
|
}
|
|
147
150
|
writeAtomic(path, data, options = {}) {
|
|
151
|
+
refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
|
|
148
152
|
mkdirSync(dirname(path), { recursive: true });
|
|
149
153
|
const tmp = `${path}.${process.pid}.${Date.now()}.${fsAtomicSequence++}.tmp`;
|
|
150
154
|
try {
|
|
@@ -161,6 +165,7 @@ export class FsWorldStore {
|
|
|
161
165
|
return existsSync(path);
|
|
162
166
|
}
|
|
163
167
|
remove(path) {
|
|
168
|
+
refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
|
|
164
169
|
rmSync(path, { recursive: true, force: true });
|
|
165
170
|
}
|
|
166
171
|
mkdir(dirPath) {
|
|
@@ -339,16 +344,19 @@ export class MemoryWorldStore {
|
|
|
339
344
|
return content === undefined ? [] : content.split('\n');
|
|
340
345
|
}
|
|
341
346
|
append(path, data) {
|
|
347
|
+
refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
|
|
342
348
|
this.files.set(path, (this.files.get(path) ?? '') + data);
|
|
343
349
|
this.sizes.set(path, (this.sizes.get(path) ?? 0) + byteLength(data));
|
|
344
350
|
this.bump(path);
|
|
345
351
|
}
|
|
346
352
|
write(path, data) {
|
|
353
|
+
refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
|
|
347
354
|
this.files.set(path, data);
|
|
348
355
|
this.sizes.set(path, byteLength(data));
|
|
349
356
|
this.bump(path);
|
|
350
357
|
}
|
|
351
358
|
writeAtomic(path, data) {
|
|
359
|
+
refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
|
|
352
360
|
// Atomic by nature in a single process: the assignment is indivisible, so no reader
|
|
353
361
|
// ever observes a partial document.
|
|
354
362
|
this.files.set(path, data);
|
|
@@ -366,6 +374,7 @@ export class MemoryWorldStore {
|
|
|
366
374
|
return false;
|
|
367
375
|
}
|
|
368
376
|
remove(path) {
|
|
377
|
+
refuseReadOnlyWrite('the World'); // a read-only request writes nothing, whatever path it takes to the store
|
|
369
378
|
let removedAny = false;
|
|
370
379
|
if (this.files.delete(path)) {
|
|
371
380
|
this.versions.delete(path);
|
package/dist/vendor-hosts.cjs
CHANGED
|
@@ -77,13 +77,14 @@ const isGoogleOAuthTokenPath = (p) =>
|
|
|
77
77
|
// operation must fail LOCALLY, not succeed remotely.
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* The www.googleapis.com paths it serves: the v1 (PEM) and v3 (JWK) cert endpoints,
|
|
81
|
-
* userinfo alias
|
|
82
|
-
*
|
|
83
|
-
* (
|
|
80
|
+
* The www.googleapis.com paths it serves: the v1 (PEM) and v3 (JWK) cert endpoints, the v3
|
|
81
|
+
* userinfo alias, and the OAuth2 API v2's userinfo (`/oauth2/v2/userinfo` and its `/userinfo/v2/me`
|
|
82
|
+
* alias, a different field set: `googleoauth.endpoints.userinfo_v2`), which `@googleapis/oauth2`
|
|
83
|
+
* calls (Cal.com's Google Calendar callback). NOT `/oauth2/v2/certs`: Google publishes no v2 certs
|
|
84
|
+
* endpoint at all.
|
|
84
85
|
*/
|
|
85
86
|
const isGoogleOAuthApisPath = (p) =>
|
|
86
|
-
typeof p === 'string' && /^\/oauth2\/(v1\/certs|v3\/(certs|userinfo))\/?$/.test(p);
|
|
87
|
+
typeof p === 'string' && /^\/(oauth2\/(v1\/certs|v3\/(certs|userinfo)|v2\/userinfo)|userinfo\/v2\/me)\/?$/.test(p);
|
|
87
88
|
|
|
88
89
|
// vendor → predicate(hostname, pathname?). A vendor is only active if its twin URL is set.
|
|
89
90
|
// THE HAND TABLE'S ONLY RESIDENTS: keys served by KERNEL packages (browser-assets serves
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
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",
|
package/src/actions.ts
CHANGED
|
@@ -21,7 +21,9 @@ import { appendDurable, eventsLockPath, ownerStoreRoots, projectionLockPath, twi
|
|
|
21
21
|
import { landAsPlaceholder, placeholderPullActive } from './placeholder-remote.ts';
|
|
22
22
|
import { aliasesFrom, dropCheckpoint, landedCopy, landedIds, parentEntries, readTree, treeStamp, type Receipt } from './log.ts';
|
|
23
23
|
import { WorldServiceEventSchema } from './schemas.ts';
|
|
24
|
+
import { refuseReadOnlyWrite } from './request-scope.ts';
|
|
24
25
|
import { getActiveWorldStore } from './world-store.ts';
|
|
26
|
+
import { currentTraceparent } from './trace-context.ts';
|
|
25
27
|
import type { WorldServiceEvent } from './types.ts';
|
|
26
28
|
import type { TwinResource } from './serve.ts';
|
|
27
29
|
|
|
@@ -94,6 +96,9 @@ export type TwinAction = {
|
|
|
94
96
|
* alone, with no dependence on actionId/pushId naming conventions.
|
|
95
97
|
*/
|
|
96
98
|
correlationId?: string;
|
|
99
|
+
/** The W3C `traceparent` of the request that caused this action, when it carried a valid one
|
|
100
|
+
* (trace-context.ts). Authoring metadata like correlationId: excluded from replay identity. */
|
|
101
|
+
traceparent?: string;
|
|
97
102
|
/**
|
|
98
103
|
* The action's MERGE BASE against the remote (runtime contract R14, non-fast-forward
|
|
99
104
|
* rule): per touched subject, the observed mirror's remote ref (shadow.ts remoteRefs)
|
|
@@ -129,7 +134,10 @@ function withCorrelationId(action: TwinAction): TwinAction {
|
|
|
129
134
|
// The wire's request id (the addressed service stamps every response with one and threads it
|
|
130
135
|
// into the handler's async context) is the correlation when the caller supplies none — the
|
|
131
136
|
// join from a request on the wire to the action rows it caused, with no pack involved.
|
|
132
|
-
|
|
137
|
+
const correlated = action.correlationId ? action : { ...action, correlationId: currentCorrelationId() ?? randomUUID() };
|
|
138
|
+
// the request's W3C trace context (trace-context.ts) is stamped the same way, from the handler's async context
|
|
139
|
+
const traceparent = correlated.traceparent === undefined ? currentTraceparent() : undefined;
|
|
140
|
+
return traceparent ? { ...correlated, traceparent } : correlated;
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
/** Subjects a `set` action touches: its own subject, every projection resource, and every
|
|
@@ -200,7 +208,7 @@ function landIfPlaceholderPull(action: TwinAction, root?: string): TwinAction |
|
|
|
200
208
|
}
|
|
201
209
|
|
|
202
210
|
function replayBody(action: TwinAction): string {
|
|
203
|
-
const { correlationId: _correlationId, ...stampedBody } = action;
|
|
211
|
+
const { correlationId: _correlationId, traceparent: _traceparent, ...stampedBody } = action;
|
|
204
212
|
// A confirmation is content-addressed by the observed event ids. Its timestamp is observation
|
|
205
213
|
// metadata, just like the occurredAt/observedAt values that appendEvent ignores when replaying
|
|
206
214
|
// one content-addressed event. Two reconcilers confirming the same bytes at different wall-clock
|
|
@@ -233,6 +241,7 @@ export function runWithCorrelationId<T>(id: string, fn: () => T): T { return cor
|
|
|
233
241
|
export function currentCorrelationId(): string | undefined { return correlationScope().getStore(); }
|
|
234
242
|
|
|
235
243
|
export function appendAction(action: TwinAction, root?: string): TwinAction {
|
|
244
|
+
refuseReadOnlyWrite(action.service); // a read-only request writes nothing (request-scope.ts)
|
|
236
245
|
// Evaluate and append under the service projection and actions locks. A precondition is a
|
|
237
246
|
// compare-and-set, not an advisory validation: checking outside either lock would let another
|
|
238
247
|
// writer invalidate it before this action lands. The shadow basis is stamped under the same
|
|
@@ -252,6 +261,7 @@ export function appendAction(action: TwinAction, root?: string): TwinAction {
|
|
|
252
261
|
* atomic across processes (two concurrent identical writes converge to ONE action; distinct
|
|
253
262
|
* writes both land). A reused id with different content fails loudly. */
|
|
254
263
|
export function appendActionIfAbsent(action: TwinAction, root?: string): { action: TwinAction; appended: boolean; placeholder?: true } {
|
|
264
|
+
refuseReadOnlyWrite(action.service);
|
|
255
265
|
return withFileLock(projectionLock(action.service, root), () => withFileLock(actionsLock(action.service, root), () => {
|
|
256
266
|
const identified = withCorrelationId(action);
|
|
257
267
|
// An exact retry is already committed. Resolve it before re-evaluating author-time
|
|
@@ -285,6 +295,7 @@ export function appendActionIfAbsent(action: TwinAction, root?: string): { actio
|
|
|
285
295
|
* the same ordinals, which is what serve-path determinism requires.
|
|
286
296
|
*/
|
|
287
297
|
export function appendActionOccurrence(base: TwinAction, root?: string): { action: TwinAction; appended: boolean; placeholder?: true } {
|
|
298
|
+
refuseReadOnlyWrite(base.service);
|
|
288
299
|
return withFileLock(projectionLock(base.service, root), () => withFileLock(actionsLock(base.service, root), () => {
|
|
289
300
|
const stamped = occurrenceOf(base, actionIds(base.service, root), root);
|
|
290
301
|
assertPreconditions(stamped, root);
|
|
@@ -345,6 +356,7 @@ export function decideAndAppendAction<T>(
|
|
|
345
356
|
const resources = projectResources(service, root);
|
|
346
357
|
const decision = decide(resources);
|
|
347
358
|
if (decision.kind === 'skip') return { value: decision.value, appended: false };
|
|
359
|
+
refuseReadOnlyWrite(service); // a decision to write is the write a read-only request may not make
|
|
348
360
|
if (decision.action.service !== service) {
|
|
349
361
|
throw new Error(`Atomic action service mismatch: expected ${service}, got ${decision.action.service}`);
|
|
350
362
|
}
|
|
@@ -525,6 +537,7 @@ export function confirmAction(opts: {
|
|
|
525
537
|
occurredAt: string;
|
|
526
538
|
root?: string;
|
|
527
539
|
}): { observedEventId: string; observedEventIds: string[] } {
|
|
540
|
+
refuseReadOnlyWrite(opts.service);
|
|
528
541
|
// LANDING (log.ts): the entry is copied to the parent log with its receipt — no confirm row, no
|
|
529
542
|
// suppression; the fold skips a branch entry the parent holds.
|
|
530
543
|
const paths = worldPaths(opts.service, opts.root);
|
|
@@ -561,6 +574,7 @@ export type RevertOutcome =
|
|
|
561
574
|
* Only `set` rows are revertable — reverting a revert or a confirm is a category error.
|
|
562
575
|
*/
|
|
563
576
|
export function revertAction(opts: { service: string; actionId: string; occurredAt: string; root?: string }): RevertOutcome {
|
|
577
|
+
refuseReadOnlyWrite(opts.service);
|
|
564
578
|
return withFileLock(projectionLock(opts.service, opts.root), () => withFileLock(actionsLock(opts.service, opts.root), (): RevertOutcome => {
|
|
565
579
|
const all = listActions(opts.service, opts.root);
|
|
566
580
|
const target = all.find((a) => a.id === opts.actionId);
|
package/src/blob-store.ts
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.ts';
|
|
25
25
|
import { sharedBlobIndex } from './shared-blob-index.ts';
|
|
26
|
+
import { refuseReadOnlyWrite, writesRefused } from './request-scope.ts';
|
|
26
27
|
|
|
27
28
|
export interface BlobStore {
|
|
28
29
|
/** Bytes at `key`, or null when absent. Never throws for a missing key. */
|
|
@@ -165,8 +166,21 @@ export function blobDigest(bytes: Uint8Array): string {
|
|
|
165
166
|
// (or a test) swaps in its adapter for the scope of a request.
|
|
166
167
|
let activeBlobStore: BlobStore = new FsBlobStore();
|
|
167
168
|
|
|
169
|
+
/** The active store; under a read-only request's refusing scope (request-scope.ts), one that refuses
|
|
170
|
+
* to store or remove bytes before any reach it, as the log's appenders refuse (a git push's objects,
|
|
171
|
+
* an upload's body), while reads pass. */
|
|
168
172
|
export function getActiveBlobStore(): BlobStore {
|
|
169
|
-
return activeBlobStore;
|
|
173
|
+
return writesRefused() ? readOnlyBlobs(activeBlobStore) : activeBlobStore;
|
|
174
|
+
}
|
|
175
|
+
function readOnlyBlobs(store: BlobStore): BlobStore {
|
|
176
|
+
return {
|
|
177
|
+
get: (key) => store.get(key),
|
|
178
|
+
exists: (key) => store.exists(key),
|
|
179
|
+
list: (prefix) => store.list(prefix),
|
|
180
|
+
size: (key) => store.size(key),
|
|
181
|
+
put: async () => { refuseReadOnlyWrite('blobs'); },
|
|
182
|
+
remove: async () => { refuseReadOnlyWrite('blobs'); },
|
|
183
|
+
};
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
export function setActiveBlobStore(store: BlobStore): BlobStore {
|
package/src/client-bundle.ts
CHANGED
|
@@ -5,9 +5,22 @@
|
|
|
5
5
|
// is retried on the next call. Nothing here runs at module scope.
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
|
|
8
|
+
/** A file URL's path on this machine, without `node:url` (the modules that call it ride into browser
|
|
9
|
+
* bundles): percent-escapes decoded, and without the slash `URL.pathname` puts before a Windows drive
|
|
10
|
+
* (`/C:/…`), which no file API opens. `new URL('../client/x.css', import.meta.url).pathname` is this. */
|
|
11
|
+
export function filePathOf(url: URL): string {
|
|
12
|
+
const path = decodeURIComponent(url.pathname);
|
|
13
|
+
return /^\/[A-Za-z]:\//.test(path) ? path.slice(1) : path;
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
const bundles = new Map<string, Promise<string>>();
|
|
9
17
|
type BunBuild = { build: (o: { entrypoints: string[]; target: 'browser'; minify: boolean }) => Promise<{ success: boolean; logs: Array<{ message: string }>; outputs: Array<{ text: () => Promise<string> }> }> };
|
|
10
18
|
|
|
19
|
+
/** A browser has no `process`. A client that imports shared helpers from its pack's server-side module can pull Node
|
|
20
|
+
* code into its bundle (util.deprecate, crypto shims) that reads it; without a shim the page throws before it renders
|
|
21
|
+
* (newer Bun no longer adds one to a browser build). A minimal one, the page's own, before the bundle. */
|
|
22
|
+
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';
|
|
23
|
+
|
|
11
24
|
export function bundleClient(entry: string): Promise<string> {
|
|
12
25
|
let pending = bundles.get(entry);
|
|
13
26
|
if (!pending) {
|
|
@@ -15,12 +28,12 @@ export function bundleClient(entry: string): Promise<string> {
|
|
|
15
28
|
pending = (bun && typeof bun.build === 'function'
|
|
16
29
|
? (bun as BunBuild).build({ entrypoints: [entry], target: 'browser', minify: true }).then(async (result) => {
|
|
17
30
|
if (!result.success) throw new Error(result.logs.map((l) => l.message).join('\n') || `client build failed: ${entry}`);
|
|
18
|
-
return result.outputs[0]!.text();
|
|
31
|
+
return PROCESS_SHIM + await result.outputs[0]!.text();
|
|
19
32
|
})
|
|
20
33
|
: Promise.resolve().then(() => {
|
|
21
34
|
const prebuilt = entry.replace(/\.tsx?$/, '.bundle.js');
|
|
22
35
|
if (!existsSync(prebuilt)) throw new Error(`client bundle missing at ${prebuilt} — the package's \`build\` writes it (scripts/publish/build.mjs); Node serves the prebuilt client`);
|
|
23
|
-
return readFileSync(prebuilt, 'utf8');
|
|
36
|
+
return PROCESS_SHIM + readFileSync(prebuilt, 'utf8');
|
|
24
37
|
}))
|
|
25
38
|
.catch((error) => { bundles.delete(entry); throw error; });
|
|
26
39
|
bundles.set(entry, pending);
|
package/src/derived-core.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { hashFieldValue } from './hash.ts';
|
|
|
14
14
|
import { resolveSubjectId, subjectAliases } from './actions.ts';
|
|
15
15
|
import { packReferences } from './references.ts';
|
|
16
16
|
import type { SubjectFields } from './hash.ts';
|
|
17
|
-
import { twinPublicBase } from './twin-fetch.ts';
|
|
17
|
+
import { twinPublicBase, withRequestScopes } from './twin-fetch.ts';
|
|
18
|
+
import { isReadOnlyRequest } from './request-scope.ts';
|
|
18
19
|
import type { DerivedCall, DerivedCoreOutcome, DerivedHandler, DerivedOperation } from './derived.ts';
|
|
19
20
|
|
|
20
21
|
// ── the manifest ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -965,8 +966,10 @@ export function crossCutting(m: DerivedManifest, opts: CoreScope & { readOnly?:
|
|
|
965
966
|
}
|
|
966
967
|
}
|
|
967
968
|
// a read-only twin refuses writes: what the operation does, not the HTTP verb it came by (an RPC
|
|
968
|
-
// wire POSTs its reads)
|
|
969
|
-
|
|
969
|
+
// wire POSTs its reads). A read-only REQUEST (x-volter-read-only, request-scope.ts) is refused the
|
|
970
|
+
// same way, up front, on a writable twin.
|
|
971
|
+
const readOnly = opts.readOnly || isReadOnlyRequest(request);
|
|
972
|
+
if (readOnly && !['retrieve', 'list', 'computed'].includes(call.operation.class) && !m.reads?.includes(call.operation.id)) return vendorError(m, m.readOnly);
|
|
970
973
|
// a body labelled JSON that does not parse is the vendor's refusal, never a crash of the twin
|
|
971
974
|
if ((request.headers.get('content-type') ?? '').includes('json') || m.body.json === 'always') {
|
|
972
975
|
const text = request.method === 'GET' || request.method === 'HEAD' ? '' : await request.clone().text().catch(() => '');
|
|
@@ -988,6 +991,8 @@ export function crossCutting(m: DerivedManifest, opts: CoreScope & { readOnly?:
|
|
|
988
991
|
// a streamed answer replays as the stream it was; a record kept before text was kept replays its JSON
|
|
989
992
|
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 });
|
|
990
993
|
}
|
|
994
|
+
// a read-only request replays a stored answer but records none: recording is a write
|
|
995
|
+
if (readOnly) return next();
|
|
991
996
|
const response = await next();
|
|
992
997
|
if (m.idempotency.onlySuccess && (response.status < 200 || response.status >= 300)) return response;
|
|
993
998
|
const contentType = response.headers.get('content-type') ?? 'application/json';
|
|
@@ -997,6 +1002,15 @@ export function crossCutting(m: DerivedManifest, opts: CoreScope & { readOnly?:
|
|
|
997
1002
|
}
|
|
998
1003
|
}
|
|
999
1004
|
|
|
1005
|
+
/** A derived pack's whole fetch behind the kernel's read scope (twin-fetch.ts withRequestScopes): a
|
|
1006
|
+
* read-only request's writes are refused at the write seam — the ones `crossCutting` cannot see
|
|
1007
|
+
* coming (another wire's mutation, a door, a screen) — and answered with the vendor's own read-only
|
|
1008
|
+
* error; `GET /twin` advertises `requestScopes`. The pack's catch-up runs under `runAsVendorMove`
|
|
1009
|
+
* (request-scope.ts), so time's moves still land when a read-only request is what arrives. */
|
|
1010
|
+
export function derivedRequestScopes<F extends (request: Request) => Promise<Response>>(m: DerivedManifest, fetch: F): F {
|
|
1011
|
+
return withRequestScopes(fetch, { refuse: () => vendorError(m, m.readOnly) });
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1000
1014
|
/** A semantics context for a request no surface operation names: another wire's (GraphQL) resolvers
|
|
1001
1015
|
* get the same interface as a handler, named by the operation id the wire gives. */
|
|
1002
1016
|
export function semanticsContext(m: DerivedManifest, request: Request, operation: DerivedOperation, scope: CoreScope = {}): Promise<SemanticsContext> {
|
package/src/derived.ts
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.ts';
|
|
8
9
|
|
|
9
10
|
/** The slice of a generated surface the dispatch reads. */
|
|
10
11
|
export type DerivedOperation = {
|
|
@@ -141,7 +142,9 @@ export function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch {
|
|
|
141
142
|
const routes = compileSurface(options.surface);
|
|
142
143
|
const fallback = (request: Request, operation: DerivedOperation | undefined, reason: string): Promise<Response> =>
|
|
143
144
|
options.legacy ? options.legacy(request) : Promise.resolve(options.gap(request, operation, reason));
|
|
144
|
-
|
|
145
|
+
// a valid incoming W3C traceparent scopes the dispatch, so every entry it appends records it (trace-context.ts)
|
|
146
|
+
const fetch = ((request: Request): Promise<Response> => runWithRequestTrace(request, () => dispatch(request))) as DerivedFetch;
|
|
147
|
+
const dispatch = async (request: Request): Promise<Response> => {
|
|
145
148
|
const url = new URL(request.url);
|
|
146
149
|
const matched = options.anyMethod
|
|
147
150
|
? [...routes.keys()].map((m) => matchOperation(routes, m, url.pathname, url.searchParams, request.headers)).find(Boolean)
|
|
@@ -167,7 +170,7 @@ export function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch {
|
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
return fallback(request, matched.operation, 'no handler');
|
|
170
|
-
}
|
|
173
|
+
};
|
|
171
174
|
fetch.owners = () =>
|
|
172
175
|
Object.fromEntries(
|
|
173
176
|
options.surface.operations.map((o) => [o.id, handlers[o.id] ? 'handler' : options.core?.owns(o) ? 'core' : options.legacy ? 'legacy' : 'gap'] as const),
|
package/src/git/refs.ts
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
// state a snapshot carries, a projection can read, and (through the action log the plane keeps
|
|
4
4
|
// beside it) a push policy can gate. No packed-refs: the store lists directories.
|
|
5
5
|
import type { WorldStore } from '../world-store.ts';
|
|
6
|
+
import { refuseReadOnlyWrite } from '../request-scope.ts';
|
|
6
7
|
|
|
7
8
|
export class GitRefs {
|
|
8
9
|
constructor(private readonly store: WorldStore, private readonly prefix: string) {}
|
|
9
10
|
private path(name: string): string { return `${this.prefix}/${name}`; }
|
|
10
11
|
get(name: string): string | null { const v = this.store.read(this.path(name)); return v === null ? null : v.trim(); }
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// a read-only request moves no ref (request-scope.ts): refused before the store is touched
|
|
13
|
+
set(name: string, sha: string): void { refuseReadOnlyWrite('git'); this.store.write(this.path(name), `${sha}\n`); }
|
|
14
|
+
delete(name: string): void { refuseReadOnlyWrite('git'); if (this.store.exists(this.path(name))) this.store.remove(this.path(name)); }
|
|
13
15
|
head(): string { const v = this.store.read(this.path('HEAD')); return v === null ? 'refs/heads/main' : v.trim().replace(/^ref: /, ''); }
|
|
14
|
-
setHead(target: string): void { this.store.write(this.path('HEAD'), `ref: ${target}\n`); }
|
|
16
|
+
setHead(target: string): void { 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<T>(fn: () => T): T { return this.store.withLock(this.path('refs.lock'), fn); }
|
|
17
19
|
/** Every `refs/...` name with its sha, sorted by name. */
|