@volter/world-core 2.0.8 → 2.0.9
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/src/rateBudget.d.ts +5 -0
- package/dist/src/rateBudget.js +25 -2
- package/dist/src/twin-fetch.js +11 -2
- package/package.json +1 -1
- package/src/rateBudget.ts +22 -2
- package/src/twin-fetch.ts +11 -1
package/dist/src/rateBudget.d.ts
CHANGED
|
@@ -301,6 +301,11 @@ export declare class RateBudget {
|
|
|
301
301
|
*/
|
|
302
302
|
private now;
|
|
303
303
|
private lockPath;
|
|
304
|
+
/** The store the ledger persists through where there is no filesystem of this machine's to hold it: a hosted World's
|
|
305
|
+
* (its SQL store, one instance serializing its requests). Null on the filesystem, where the ledger is the file below,
|
|
306
|
+
* locked across processes as it always was. A hosted World's ledger is its own: two hosted Worlds sealed with one
|
|
307
|
+
* credential each keep an allowance, where a machine's Worlds share one per credential. */
|
|
308
|
+
private store;
|
|
304
309
|
/** Acquire the exclusive lock, run `fn`, always release. Bounded wait, dead locks broken. */
|
|
305
310
|
private withLock;
|
|
306
311
|
/** Remove a lock whose owner is provably gone. Never removes one that might still be live. */
|
package/dist/src/rateBudget.js
CHANGED
|
@@ -72,6 +72,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
72
72
|
import { volterHome } from "./volter-home.js";
|
|
73
73
|
import { dirname, join } from 'node:path';
|
|
74
74
|
import { worldPaths } from "./storage.js";
|
|
75
|
+
import { FsWorldStore, getActiveWorldStore } from "./world-store.js";
|
|
75
76
|
// ── bounds the kernel enforces on ANY declaration ───────────────────────────────────────────
|
|
76
77
|
/** Shortest window a declaration may claim. A sub-second window is a counter reset, not a budget. */
|
|
77
78
|
export const MIN_RATE_BUDGET_WINDOW_MS = 1_000;
|
|
@@ -538,8 +539,19 @@ export class RateBudget {
|
|
|
538
539
|
}
|
|
539
540
|
// ── locking + durable IO ──────────────────────────────────────────────────────────────────
|
|
540
541
|
lockPath() { return `${this.path}.lock`; }
|
|
542
|
+
/** The store the ledger persists through where there is no filesystem of this machine's to hold it: a hosted World's
|
|
543
|
+
* (its SQL store, one instance serializing its requests). Null on the filesystem, where the ledger is the file below,
|
|
544
|
+
* locked across processes as it always was. A hosted World's ledger is its own: two hosted Worlds sealed with one
|
|
545
|
+
* credential each keep an allowance, where a machine's Worlds share one per credential. */
|
|
546
|
+
store() {
|
|
547
|
+
const active = getActiveWorldStore();
|
|
548
|
+
return active instanceof FsWorldStore ? null : active;
|
|
549
|
+
}
|
|
541
550
|
/** Acquire the exclusive lock, run `fn`, always release. Bounded wait, dead locks broken. */
|
|
542
551
|
withLock(fn) {
|
|
552
|
+
const held = this.store();
|
|
553
|
+
if (held)
|
|
554
|
+
return held.withLock(this.lockPath(), fn);
|
|
543
555
|
const lock = this.lockPath();
|
|
544
556
|
mkdirSync(dirname(this.path), { recursive: true });
|
|
545
557
|
let fd;
|
|
@@ -613,11 +625,12 @@ export class RateBudget {
|
|
|
613
625
|
* window instead of needing a human to delete a file.
|
|
614
626
|
*/
|
|
615
627
|
load(now) {
|
|
616
|
-
|
|
628
|
+
const held = this.store();
|
|
629
|
+
if (held ? !held.exists(this.path) : !existsSync(this.path))
|
|
617
630
|
return { v: 1, cooldownUntil: 0, entries: [] };
|
|
618
631
|
let raw;
|
|
619
632
|
try {
|
|
620
|
-
raw = readFileSync(this.path, 'utf8');
|
|
633
|
+
raw = held ? (held.read(this.path) ?? '') : readFileSync(this.path, 'utf8');
|
|
621
634
|
}
|
|
622
635
|
catch (e) {
|
|
623
636
|
throw this.quarantine(now, `unreadable (${e.message})`);
|
|
@@ -668,6 +681,16 @@ export class RateBudget {
|
|
|
668
681
|
}
|
|
669
682
|
/** Atomic replace: write a temp file, then rename over the target. No partial ledger is visible. */
|
|
670
683
|
save(ledger) {
|
|
684
|
+
const held = this.store();
|
|
685
|
+
if (held) {
|
|
686
|
+
try {
|
|
687
|
+
held.writeAtomic(this.path, `${JSON.stringify(ledger)}\n`);
|
|
688
|
+
}
|
|
689
|
+
catch (e) {
|
|
690
|
+
throw new RateBudgetError('ledger-unwritable', this.vendor, `${this.vendor} budget: cannot persist ${this.path} (${e.message}) — refusing rather than tracking nothing`, this.windowMs);
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
671
694
|
const tmp = `${this.path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
672
695
|
try {
|
|
673
696
|
mkdirSync(dirname(this.path), { recursive: true });
|
package/dist/src/twin-fetch.js
CHANGED
|
@@ -22,8 +22,17 @@ export const TWIN_PREFIX_HEADER = 'x-forwarded-prefix';
|
|
|
22
22
|
* callback) from this, never from the bare origin, so the URL works behind a served World. */
|
|
23
23
|
export function twinPublicBase(request) {
|
|
24
24
|
const prefix = request.headers.get(TWIN_PREFIX_HEADER);
|
|
25
|
-
if (!prefix || !/^\/[A-Za-z0-9._~\/-]*$/.test(prefix))
|
|
26
|
-
|
|
25
|
+
if (!prefix || !/^\/[A-Za-z0-9._~\/-]*$/.test(prefix)) {
|
|
26
|
+
// a twin reached at its vendor's host through the World's proxy, which ends the caller's TLS and forwards the
|
|
27
|
+
// request in the clear: the scheme the caller used is the one its links carry (a page's form posting to
|
|
28
|
+
// http://accounts.google.com from an https page is one a browser warns about), the host the request's own
|
|
29
|
+
// A twin reached at its own listener (a loopback or an address: the injector's route, a direct client) is that
|
|
30
|
+
// listener, whatever scheme a caller's forwarded headers claim
|
|
31
|
+
const proto = request.headers.get('x-forwarded-proto');
|
|
32
|
+
const own = new URL(request.url);
|
|
33
|
+
const listener = own.hostname === 'localhost' || own.hostname.endsWith('.localhost') || /^[\d.]+$/.test(own.hostname) || own.hostname.startsWith('[');
|
|
34
|
+
return !listener && (proto === 'https' || proto === 'http') ? `${proto}://${own.host}` : own.origin;
|
|
35
|
+
}
|
|
27
36
|
// the host that mounted the twin says where it is reached: a local World forwards to the twin's
|
|
28
37
|
// own port, so the request's own origin is not the public one
|
|
29
38
|
const host = request.headers.get('x-forwarded-host');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
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/rateBudget.ts
CHANGED
|
@@ -72,6 +72,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
72
72
|
import { volterHome } from './volter-home.ts';
|
|
73
73
|
import { dirname, join } from 'node:path';
|
|
74
74
|
import { worldPaths } from './storage.ts';
|
|
75
|
+
import { FsWorldStore, getActiveWorldStore, type WorldStore } from './world-store.ts';
|
|
75
76
|
|
|
76
77
|
// ── bounds the kernel enforces on ANY declaration ───────────────────────────────────────────
|
|
77
78
|
|
|
@@ -706,8 +707,19 @@ export class RateBudget {
|
|
|
706
707
|
|
|
707
708
|
private lockPath(): string { return `${this.path}.lock`; }
|
|
708
709
|
|
|
710
|
+
/** The store the ledger persists through where there is no filesystem of this machine's to hold it: a hosted World's
|
|
711
|
+
* (its SQL store, one instance serializing its requests). Null on the filesystem, where the ledger is the file below,
|
|
712
|
+
* locked across processes as it always was. A hosted World's ledger is its own: two hosted Worlds sealed with one
|
|
713
|
+
* credential each keep an allowance, where a machine's Worlds share one per credential. */
|
|
714
|
+
private store(): WorldStore | null {
|
|
715
|
+
const active = getActiveWorldStore();
|
|
716
|
+
return active instanceof FsWorldStore ? null : active;
|
|
717
|
+
}
|
|
718
|
+
|
|
709
719
|
/** Acquire the exclusive lock, run `fn`, always release. Bounded wait, dead locks broken. */
|
|
710
720
|
private withLock<T>(fn: () => T): T {
|
|
721
|
+
const held = this.store();
|
|
722
|
+
if (held) return held.withLock(this.lockPath(), fn);
|
|
711
723
|
const lock = this.lockPath();
|
|
712
724
|
mkdirSync(dirname(this.path), { recursive: true });
|
|
713
725
|
let fd: number | undefined;
|
|
@@ -764,10 +776,11 @@ export class RateBudget {
|
|
|
764
776
|
* window instead of needing a human to delete a file.
|
|
765
777
|
*/
|
|
766
778
|
private load(now: number): Ledger {
|
|
767
|
-
|
|
779
|
+
const held = this.store();
|
|
780
|
+
if (held ? !held.exists(this.path) : !existsSync(this.path)) return { v: 1, cooldownUntil: 0, entries: [] };
|
|
768
781
|
let raw: string;
|
|
769
782
|
try {
|
|
770
|
-
raw = readFileSync(this.path, 'utf8');
|
|
783
|
+
raw = held ? (held.read(this.path) ?? '') : readFileSync(this.path, 'utf8');
|
|
771
784
|
} catch (e) {
|
|
772
785
|
throw this.quarantine(now, `unreadable (${(e as Error).message})`);
|
|
773
786
|
}
|
|
@@ -819,6 +832,13 @@ export class RateBudget {
|
|
|
819
832
|
|
|
820
833
|
/** Atomic replace: write a temp file, then rename over the target. No partial ledger is visible. */
|
|
821
834
|
private save(ledger: Ledger): void {
|
|
835
|
+
const held = this.store();
|
|
836
|
+
if (held) {
|
|
837
|
+
try { held.writeAtomic(this.path, `${JSON.stringify(ledger)}\n`); } catch (e) {
|
|
838
|
+
throw new RateBudgetError('ledger-unwritable', this.vendor, `${this.vendor} budget: cannot persist ${this.path} (${(e as Error).message}) — refusing rather than tracking nothing`, this.windowMs);
|
|
839
|
+
}
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
822
842
|
const tmp = `${this.path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
823
843
|
try {
|
|
824
844
|
mkdirSync(dirname(this.path), { recursive: true });
|
package/src/twin-fetch.ts
CHANGED
|
@@ -24,7 +24,17 @@ export const TWIN_PREFIX_HEADER = 'x-forwarded-prefix';
|
|
|
24
24
|
* callback) from this, never from the bare origin, so the URL works behind a served World. */
|
|
25
25
|
export function twinPublicBase(request: Request): string {
|
|
26
26
|
const prefix = request.headers.get(TWIN_PREFIX_HEADER);
|
|
27
|
-
if (!prefix || !/^\/[A-Za-z0-9._~\/-]*$/.test(prefix))
|
|
27
|
+
if (!prefix || !/^\/[A-Za-z0-9._~\/-]*$/.test(prefix)) {
|
|
28
|
+
// a twin reached at its vendor's host through the World's proxy, which ends the caller's TLS and forwards the
|
|
29
|
+
// request in the clear: the scheme the caller used is the one its links carry (a page's form posting to
|
|
30
|
+
// http://accounts.google.com from an https page is one a browser warns about), the host the request's own
|
|
31
|
+
// A twin reached at its own listener (a loopback or an address: the injector's route, a direct client) is that
|
|
32
|
+
// listener, whatever scheme a caller's forwarded headers claim
|
|
33
|
+
const proto = request.headers.get('x-forwarded-proto');
|
|
34
|
+
const own = new URL(request.url);
|
|
35
|
+
const listener = own.hostname === 'localhost' || own.hostname.endsWith('.localhost') || /^[\d.]+$/.test(own.hostname) || own.hostname.startsWith('[');
|
|
36
|
+
return !listener && (proto === 'https' || proto === 'http') ? `${proto}://${own.host}` : own.origin;
|
|
37
|
+
}
|
|
28
38
|
// the host that mounted the twin says where it is reached: a local World forwards to the twin's
|
|
29
39
|
// own port, so the request's own origin is not the public one
|
|
30
40
|
const host = request.headers.get('x-forwarded-host');
|