@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.
@@ -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. */
@@ -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
- if (!existsSync(this.path))
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 });
@@ -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
- return new URL(request.url).origin;
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.8",
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
- if (!existsSync(this.path)) return { v: 1, cooldownUntil: 0, entries: [] };
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)) return new URL(request.url).origin;
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');