@volter/world-core 2.0.7 → 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/app-route.cjs +46 -21
- package/app-route.d.cts +2 -2
- package/dist/app-route.cjs +46 -21
- package/dist/app-route.d.cts +2 -2
- package/dist/inject.cjs +7 -22
- package/dist/src/rateBudget.d.ts +5 -0
- package/dist/src/rateBudget.js +25 -2
- package/dist/src/twin-fetch.js +11 -2
- package/dist/vendor-hosts.cjs +33 -1
- package/inject.cjs +7 -22
- package/package.json +1 -1
- package/src/rateBudget.ts +22 -2
- package/src/twin-fetch.ts +11 -1
- package/vendor-hosts.cjs +33 -1
package/app-route.cjs
CHANGED
|
@@ -15,39 +15,66 @@
|
|
|
15
15
|
// so a host or URL recorded after the process started applies.
|
|
16
16
|
|
|
17
17
|
// node's own modules are required where used: importing this module does nothing (a pack's egress imports it)
|
|
18
|
-
const APP_ROUTES = new Map(); // cache: instance file -> { checkedAt, mtimeMs,
|
|
18
|
+
const APP_ROUTES = new Map(); // cache: instance file -> { checkedAt, mtimeMs, routes, services, twins }
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
function
|
|
22
|
-
if (!
|
|
20
|
+
/** Each application's route of one record entry: its name, its URL (null: the World's service of its name), its hosts. */
|
|
21
|
+
function entryRoute(name, entry) {
|
|
22
|
+
if (!entry || !Array.isArray(entry.hosts) || !entry.hosts.length) return null;
|
|
23
|
+
return { name, url: typeof entry.url === 'string' ? entry.url : null, hosts: new Set(entry.hosts.map((h) => String(h).toLowerCase())) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The instance's applications (`app-url.json`: the top-level record is the application named `app`, `apps` holds the
|
|
27
|
+
* others by name) with their origins, and the twins the World runs by vendor key, read from the instance's own env:
|
|
28
|
+
* a process's env is the World's as it stood when that process started, which a twin started before others (or the
|
|
29
|
+
* twin itself) lacks. Read at most once a second. */
|
|
30
|
+
function readRoutes(instanceFile) {
|
|
31
|
+
if (!instanceFile) return { routes: [], twins: {} };
|
|
23
32
|
const now = Date.now();
|
|
24
33
|
let held = APP_ROUTES.get(instanceFile);
|
|
25
|
-
if (held && now - held.checkedAt < 1000) return held
|
|
26
|
-
if (!held) { held = { checkedAt: 0, mtimeMs: -1,
|
|
34
|
+
if (held && now - held.checkedAt < 1000) return held;
|
|
35
|
+
if (!held) { held = { checkedAt: 0, mtimeMs: -1, routes: [], services: {}, twins: {} }; APP_ROUTES.set(instanceFile, held); }
|
|
27
36
|
held.checkedAt = now;
|
|
28
37
|
const fs = require('fs');
|
|
29
38
|
const path = require('path');
|
|
30
39
|
const file = path.join(path.dirname(instanceFile), 'app-url.json');
|
|
40
|
+
// the instance (its services, and the World env its twins' URLs are in); one caught mid-write keeps the last read
|
|
41
|
+
try {
|
|
42
|
+
const instance = JSON.parse(fs.readFileSync(instanceFile, 'utf8'));
|
|
43
|
+
held.services = instance.services || {};
|
|
44
|
+
held.twins = require('./vendor-hosts.cjs').twinOrigins(instance.env || {});
|
|
45
|
+
} catch { /* no instance yet, or mid-write */ }
|
|
31
46
|
try {
|
|
32
47
|
const mtimeMs = fs.statSync(file).mtimeMs;
|
|
33
48
|
if (mtimeMs !== held.mtimeMs) {
|
|
34
49
|
held.mtimeMs = mtimeMs;
|
|
35
50
|
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
const others = record.apps && typeof record.apps === 'object' ? Object.entries(record.apps) : [];
|
|
52
|
+
held.routes = [entryRoute('app', record), ...others.map(([name, entry]) => entryRoute(name, entry))].filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
// a route with hosts and no URL names the World's service of the application's name, wherever this boot put it
|
|
55
|
+
const services = held.services;
|
|
56
|
+
for (const route of held.routes) {
|
|
57
|
+
const at = route.url ?? services[route.name]?.url;
|
|
58
|
+
route.origin = typeof at === 'string' ? at.replace(/\/$/, '') : null;
|
|
39
59
|
}
|
|
40
|
-
// a record with hosts and no URL names the World's own `app` service, wherever this boot put it
|
|
41
|
-
if (held.route && !held.route.url) {
|
|
42
|
-
const service = JSON.parse(fs.readFileSync(instanceFile, 'utf8')).services?.app;
|
|
43
|
-
held.route.origin = typeof service?.url === 'string' ? service.url.replace(/\/$/, '') : null;
|
|
44
|
-
} else if (held.route) held.route.origin = held.route.url.replace(/\/$/, '');
|
|
45
60
|
} catch (error) {
|
|
46
61
|
// gone: no route; caught mid-write: the last one read stands until the next look
|
|
47
62
|
held.mtimeMs = -1;
|
|
48
|
-
if (error && error.code === 'ENOENT') held.
|
|
63
|
+
if (error && error.code === 'ENOENT') held.routes = [];
|
|
49
64
|
}
|
|
50
|
-
return held
|
|
65
|
+
return held;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The application that answers `host` in the instance `instanceFile`, or null: never a name a vendor's host rule gives
|
|
69
|
+
* a twin the World runs, which the twin answers instead. The twins are those the instance's env points at, with the
|
|
70
|
+
* caller's own (`twins`: vendor keys as `twinOrigins` reads them, the injector's map). */
|
|
71
|
+
function appRouteFor(instanceFile, host, twins = {}) {
|
|
72
|
+
const bare = String(host || '').toLowerCase().replace(/\.$/, '');
|
|
73
|
+
const held = readRoutes(instanceFile);
|
|
74
|
+
const route = held.routes.find((r) => r.origin && appHostMatch(r.hosts, bare));
|
|
75
|
+
if (!route) return null;
|
|
76
|
+
const runs = (vendor) => Object.hasOwn(held.twins, vendor) || Object.hasOwn(twins, vendor);
|
|
77
|
+
return require('./vendor-hosts.cjs').vendorsOfHost(bare).some(runs) ? null : route;
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
/** Whether `host` is one of the app's recorded hostnames: named exactly, or under a recorded wildcard (`*.dub.link`),
|
|
@@ -70,10 +97,8 @@ function appDestination(value, env = process.env) {
|
|
|
70
97
|
let url;
|
|
71
98
|
try { url = new URL(String(value)); } catch { return null; }
|
|
72
99
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// never a name a vendor's host rule gives its twin, as the injector's resolveTwin rules
|
|
76
|
-
return route && appHostMatch(route.hosts, host) && !require('./vendor-hosts.cjs').isVendorHost(host) ? route : null;
|
|
100
|
+
// never a name a vendor's host rule gives a twin the World runs, as the injector's resolveTwin rules
|
|
101
|
+
return appRouteFor(env.VOLTER_WORLD_INSTANCE, url.hostname, require('./vendor-hosts.cjs').twinOrigins(env));
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
let SESSION_CA = undefined; // lazily read once; NODE_EXTRA_CA_CERTS set after process start
|
|
@@ -240,4 +265,4 @@ async function appFetch(value, init = {}) {
|
|
|
240
265
|
}
|
|
241
266
|
}
|
|
242
267
|
|
|
243
|
-
module.exports = {
|
|
268
|
+
module.exports = { appRouteFor, appHostMatch, appForwardHeaders, appDestination, appRequestOptions, deliverToApp, appFetch, sessionCa };
|
package/app-route.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export interface AppRoute { readonly origin: string; readonly url: string | null; readonly hosts: ReadonlySet<string> }
|
|
1
|
+
export interface AppRoute { readonly name: string; readonly origin: string; readonly url: string | null; readonly hosts: ReadonlySet<string> }
|
|
2
2
|
export interface AppAnswer { readonly status: number; readonly headers: Record<string, string>; readonly body: string }
|
|
3
|
-
export function
|
|
3
|
+
export function appRouteFor(instanceFile: string | undefined, host: string, twins?: Readonly<Record<string, string>>): AppRoute | null;
|
|
4
4
|
export function appHostMatch(hosts: ReadonlySet<string>, host: string): boolean;
|
|
5
5
|
export function appForwardHeaders(url: URL): Record<string, string>;
|
|
6
6
|
export function appDestination(value: string | URL, env?: Readonly<Record<string, string | undefined>>): AppRoute | null;
|
package/dist/app-route.cjs
CHANGED
|
@@ -15,39 +15,66 @@
|
|
|
15
15
|
// so a host or URL recorded after the process started applies.
|
|
16
16
|
|
|
17
17
|
// node's own modules are required where used: importing this module does nothing (a pack's egress imports it)
|
|
18
|
-
const APP_ROUTES = new Map(); // cache: instance file -> { checkedAt, mtimeMs,
|
|
18
|
+
const APP_ROUTES = new Map(); // cache: instance file -> { checkedAt, mtimeMs, routes, services, twins }
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
function
|
|
22
|
-
if (!
|
|
20
|
+
/** Each application's route of one record entry: its name, its URL (null: the World's service of its name), its hosts. */
|
|
21
|
+
function entryRoute(name, entry) {
|
|
22
|
+
if (!entry || !Array.isArray(entry.hosts) || !entry.hosts.length) return null;
|
|
23
|
+
return { name, url: typeof entry.url === 'string' ? entry.url : null, hosts: new Set(entry.hosts.map((h) => String(h).toLowerCase())) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The instance's applications (`app-url.json`: the top-level record is the application named `app`, `apps` holds the
|
|
27
|
+
* others by name) with their origins, and the twins the World runs by vendor key, read from the instance's own env:
|
|
28
|
+
* a process's env is the World's as it stood when that process started, which a twin started before others (or the
|
|
29
|
+
* twin itself) lacks. Read at most once a second. */
|
|
30
|
+
function readRoutes(instanceFile) {
|
|
31
|
+
if (!instanceFile) return { routes: [], twins: {} };
|
|
23
32
|
const now = Date.now();
|
|
24
33
|
let held = APP_ROUTES.get(instanceFile);
|
|
25
|
-
if (held && now - held.checkedAt < 1000) return held
|
|
26
|
-
if (!held) { held = { checkedAt: 0, mtimeMs: -1,
|
|
34
|
+
if (held && now - held.checkedAt < 1000) return held;
|
|
35
|
+
if (!held) { held = { checkedAt: 0, mtimeMs: -1, routes: [], services: {}, twins: {} }; APP_ROUTES.set(instanceFile, held); }
|
|
27
36
|
held.checkedAt = now;
|
|
28
37
|
const fs = require('fs');
|
|
29
38
|
const path = require('path');
|
|
30
39
|
const file = path.join(path.dirname(instanceFile), 'app-url.json');
|
|
40
|
+
// the instance (its services, and the World env its twins' URLs are in); one caught mid-write keeps the last read
|
|
41
|
+
try {
|
|
42
|
+
const instance = JSON.parse(fs.readFileSync(instanceFile, 'utf8'));
|
|
43
|
+
held.services = instance.services || {};
|
|
44
|
+
held.twins = require('./vendor-hosts.cjs').twinOrigins(instance.env || {});
|
|
45
|
+
} catch { /* no instance yet, or mid-write */ }
|
|
31
46
|
try {
|
|
32
47
|
const mtimeMs = fs.statSync(file).mtimeMs;
|
|
33
48
|
if (mtimeMs !== held.mtimeMs) {
|
|
34
49
|
held.mtimeMs = mtimeMs;
|
|
35
50
|
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
const others = record.apps && typeof record.apps === 'object' ? Object.entries(record.apps) : [];
|
|
52
|
+
held.routes = [entryRoute('app', record), ...others.map(([name, entry]) => entryRoute(name, entry))].filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
// a route with hosts and no URL names the World's service of the application's name, wherever this boot put it
|
|
55
|
+
const services = held.services;
|
|
56
|
+
for (const route of held.routes) {
|
|
57
|
+
const at = route.url ?? services[route.name]?.url;
|
|
58
|
+
route.origin = typeof at === 'string' ? at.replace(/\/$/, '') : null;
|
|
39
59
|
}
|
|
40
|
-
// a record with hosts and no URL names the World's own `app` service, wherever this boot put it
|
|
41
|
-
if (held.route && !held.route.url) {
|
|
42
|
-
const service = JSON.parse(fs.readFileSync(instanceFile, 'utf8')).services?.app;
|
|
43
|
-
held.route.origin = typeof service?.url === 'string' ? service.url.replace(/\/$/, '') : null;
|
|
44
|
-
} else if (held.route) held.route.origin = held.route.url.replace(/\/$/, '');
|
|
45
60
|
} catch (error) {
|
|
46
61
|
// gone: no route; caught mid-write: the last one read stands until the next look
|
|
47
62
|
held.mtimeMs = -1;
|
|
48
|
-
if (error && error.code === 'ENOENT') held.
|
|
63
|
+
if (error && error.code === 'ENOENT') held.routes = [];
|
|
49
64
|
}
|
|
50
|
-
return held
|
|
65
|
+
return held;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The application that answers `host` in the instance `instanceFile`, or null: never a name a vendor's host rule gives
|
|
69
|
+
* a twin the World runs, which the twin answers instead. The twins are those the instance's env points at, with the
|
|
70
|
+
* caller's own (`twins`: vendor keys as `twinOrigins` reads them, the injector's map). */
|
|
71
|
+
function appRouteFor(instanceFile, host, twins = {}) {
|
|
72
|
+
const bare = String(host || '').toLowerCase().replace(/\.$/, '');
|
|
73
|
+
const held = readRoutes(instanceFile);
|
|
74
|
+
const route = held.routes.find((r) => r.origin && appHostMatch(r.hosts, bare));
|
|
75
|
+
if (!route) return null;
|
|
76
|
+
const runs = (vendor) => Object.hasOwn(held.twins, vendor) || Object.hasOwn(twins, vendor);
|
|
77
|
+
return require('./vendor-hosts.cjs').vendorsOfHost(bare).some(runs) ? null : route;
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
/** Whether `host` is one of the app's recorded hostnames: named exactly, or under a recorded wildcard (`*.dub.link`),
|
|
@@ -70,10 +97,8 @@ function appDestination(value, env = process.env) {
|
|
|
70
97
|
let url;
|
|
71
98
|
try { url = new URL(String(value)); } catch { return null; }
|
|
72
99
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// never a name a vendor's host rule gives its twin, as the injector's resolveTwin rules
|
|
76
|
-
return route && appHostMatch(route.hosts, host) && !require('./vendor-hosts.cjs').isVendorHost(host) ? route : null;
|
|
100
|
+
// never a name a vendor's host rule gives a twin the World runs, as the injector's resolveTwin rules
|
|
101
|
+
return appRouteFor(env.VOLTER_WORLD_INSTANCE, url.hostname, require('./vendor-hosts.cjs').twinOrigins(env));
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
let SESSION_CA = undefined; // lazily read once; NODE_EXTRA_CA_CERTS set after process start
|
|
@@ -240,4 +265,4 @@ async function appFetch(value, init = {}) {
|
|
|
240
265
|
}
|
|
241
266
|
}
|
|
242
267
|
|
|
243
|
-
module.exports = {
|
|
268
|
+
module.exports = { appRouteFor, appHostMatch, appForwardHeaders, appDestination, appRequestOptions, deliverToApp, appFetch, sessionCa };
|
package/dist/app-route.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export interface AppRoute { readonly origin: string; readonly url: string | null; readonly hosts: ReadonlySet<string> }
|
|
1
|
+
export interface AppRoute { readonly name: string; readonly origin: string; readonly url: string | null; readonly hosts: ReadonlySet<string> }
|
|
2
2
|
export interface AppAnswer { readonly status: number; readonly headers: Record<string, string>; readonly body: string }
|
|
3
|
-
export function
|
|
3
|
+
export function appRouteFor(instanceFile: string | undefined, host: string, twins?: Readonly<Record<string, string>>): AppRoute | null;
|
|
4
4
|
export function appHostMatch(hosts: ReadonlySet<string>, host: string): boolean;
|
|
5
5
|
export function appForwardHeaders(url: URL): Record<string, string>;
|
|
6
6
|
export function appDestination(value: string | URL, env?: Readonly<Record<string, string | undefined>>): AppRoute | null;
|
package/dist/inject.cjs
CHANGED
|
@@ -42,7 +42,7 @@ const { createPublicKey, createVerify } = require('crypto');
|
|
|
42
42
|
|
|
43
43
|
// the vendor host table (VENDOR_HOSTS: a hostname, and a path where two vendors share a host, to the vendor whose twin
|
|
44
44
|
// serves it) has its own home, shared with the application route
|
|
45
|
-
const { VENDOR_HOSTS,
|
|
45
|
+
const { VENDOR_HOSTS, twinEnvStem, twinOrigins } = require('./vendor-hosts.cjs');
|
|
46
46
|
const dns = require('dns');
|
|
47
47
|
|
|
48
48
|
function decodeBase64UrlJson(part) {
|
|
@@ -186,9 +186,6 @@ function installPrismaAdapterShim() {
|
|
|
186
186
|
// answers as s3/dynamodb/timestream/sesv2/secretsmanager/bedrock — there IS no 'aws' vendor key, which is exactly the
|
|
187
187
|
// silent-inert-env trap this helper exists to make checkable). Shared with world-runtime so
|
|
188
188
|
// its "this injectEnv var is inert" warning can never drift from what readMap actually reads.
|
|
189
|
-
function twinEnvStem(vendor) {
|
|
190
|
-
return String(vendor).toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
191
|
-
}
|
|
192
189
|
|
|
193
190
|
function twinUrlVendor(name) {
|
|
194
191
|
const match = /^([A-Z][A-Z0-9_]*)_TWIN_URL$/.exec(String(name || ''));
|
|
@@ -200,20 +197,7 @@ function readMap(env) {
|
|
|
200
197
|
const map = {};
|
|
201
198
|
// the World instance whose recorded app hosts this map routes (APP HOSTS, below); not a vendor, so not a key
|
|
202
199
|
if (env.VOLTER_WORLD_INSTANCE) Object.defineProperty(map, APP_INSTANCE, { value: env.VOLTER_WORLD_INSTANCE, enumerable: false });
|
|
203
|
-
|
|
204
|
-
const url = env[`${twinEnvStem(vendor)}_TWIN_URL`];
|
|
205
|
-
if (url) map[vendor] = url.replace(/\/$/, '');
|
|
206
|
-
}
|
|
207
|
-
if (env.TWIN_INJECT) {
|
|
208
|
-
for (const pair of env.TWIN_INJECT.split(',')) {
|
|
209
|
-
const eq = pair.indexOf('=');
|
|
210
|
-
if (eq <= 0) continue;
|
|
211
|
-
const vendor = pair.slice(0, eq).trim();
|
|
212
|
-
const url = pair.slice(eq + 1).trim();
|
|
213
|
-
if (vendor && url && VENDOR_HOSTS[vendor]) map[vendor] = url.replace(/\/$/, '');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return map;
|
|
200
|
+
return Object.assign(map, twinOrigins(env));
|
|
217
201
|
}
|
|
218
202
|
|
|
219
203
|
/**
|
|
@@ -237,11 +221,12 @@ function resolveTwin(hostname, map, pathname) {
|
|
|
237
221
|
}
|
|
238
222
|
// last, the application's own production hostnames, routed to where it listens in the World; never the World's own
|
|
239
223
|
// origin or a twin's, whose requests carry the World key
|
|
240
|
-
const app = appRoute(map[APP_INSTANCE]);
|
|
241
224
|
const bare = host.replace(/\.$/, '');
|
|
242
225
|
const base = worldBase();
|
|
243
|
-
// a name a vendor's host rule gives
|
|
244
|
-
|
|
226
|
+
// a name a vendor's host rule gives a twin running here is that twin's, never an application's (a vendor with no
|
|
227
|
+
// twin here can be one of the World's applications: the product itself)
|
|
228
|
+
const app = appRouteFor(map[APP_INSTANCE], bare, map);
|
|
229
|
+
if (app && !isTwinOriginHost(bare) && (!base || new URL(base.origin).hostname !== bare)) {
|
|
245
230
|
return { vendor: 'app', origin: app.origin };
|
|
246
231
|
}
|
|
247
232
|
return null;
|
|
@@ -254,7 +239,7 @@ function resolveTwin(hostname, map, pathname) {
|
|
|
254
239
|
// was made with (appForwardHeaders), so an application serving several hostnames answers each as in production.
|
|
255
240
|
const APP_INSTANCE = Symbol.for('volter.inject.appInstance');
|
|
256
241
|
// the route itself (the record, its wildcards, the forwarded headers) has one home, shared with the twins' deliveries
|
|
257
|
-
const {
|
|
242
|
+
const { appRouteFor, appForwardHeaders, appRequestOptions, sessionCa } = require('./app-route.cjs');
|
|
258
243
|
|
|
259
244
|
// HOSTS A TWIN CLAIMS WHILE IT RUNS (packRegistry `hostsClaimed`): no descriptor can list a name a person makes answer
|
|
260
245
|
// to a vendor, so each claiming twin lists its own at its door, read here every five seconds from the moment the
|
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/dist/vendor-hosts.cjs
CHANGED
|
@@ -198,4 +198,36 @@ function isVendorHost(host) {
|
|
|
198
198
|
return Object.keys(VENDOR_HOSTS).some((vendor) => VENDOR_HOSTS[vendor](h));
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
/** The vendors whose host rule names `host` (any path): the twins that answer it while they run. */
|
|
202
|
+
function vendorsOfHost(host) {
|
|
203
|
+
const h = String(host || '').toLowerCase().replace(/\.$/, '');
|
|
204
|
+
return Object.keys(VENDOR_HOSTS).filter((vendor) => VENDOR_HOSTS[vendor](h));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A vendor key's env stem: `S3_TWIN_URL` is s3's; `googleauth` → GOOGLEAUTH. */
|
|
208
|
+
function twinEnvStem(vendor) {
|
|
209
|
+
return String(vendor).toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The vendors whose twin `env` points at, each with its origin: `<STEM>_TWIN_URL`, then `TWIN_INJECT` pairs
|
|
213
|
+
* (`vendor=url,…`). The one reading of it: the injector's map is this, and so is "the twins the World runs" wherever a
|
|
214
|
+
* route asks it (a service's id is not its vendor key: the aws service serves s3, dynamodb and others). */
|
|
215
|
+
function twinOrigins(env) {
|
|
216
|
+
const map = {};
|
|
217
|
+
for (const vendor of Object.keys(VENDOR_HOSTS)) {
|
|
218
|
+
const url = env[`${twinEnvStem(vendor)}_TWIN_URL`];
|
|
219
|
+
if (url) map[vendor] = url.replace(/\/$/, '');
|
|
220
|
+
}
|
|
221
|
+
if (env.TWIN_INJECT) {
|
|
222
|
+
for (const pair of String(env.TWIN_INJECT).split(',')) {
|
|
223
|
+
const eq = pair.indexOf('=');
|
|
224
|
+
if (eq <= 0) continue;
|
|
225
|
+
const vendor = pair.slice(0, eq).trim();
|
|
226
|
+
const url = pair.slice(eq + 1).trim();
|
|
227
|
+
if (vendor && url && Object.hasOwn(VENDOR_HOSTS, vendor)) map[vendor] = url.replace(/\/$/, '');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return map;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = { VENDOR_HOSTS, isVendorHost, vendorsOfHost, twinEnvStem, twinOrigins };
|
package/inject.cjs
CHANGED
|
@@ -42,7 +42,7 @@ const { createPublicKey, createVerify } = require('crypto');
|
|
|
42
42
|
|
|
43
43
|
// the vendor host table (VENDOR_HOSTS: a hostname, and a path where two vendors share a host, to the vendor whose twin
|
|
44
44
|
// serves it) has its own home, shared with the application route
|
|
45
|
-
const { VENDOR_HOSTS,
|
|
45
|
+
const { VENDOR_HOSTS, twinEnvStem, twinOrigins } = require('./vendor-hosts.cjs');
|
|
46
46
|
const dns = require('dns');
|
|
47
47
|
|
|
48
48
|
function decodeBase64UrlJson(part) {
|
|
@@ -186,9 +186,6 @@ function installPrismaAdapterShim() {
|
|
|
186
186
|
// answers as s3/dynamodb/timestream/sesv2/secretsmanager/bedrock — there IS no 'aws' vendor key, which is exactly the
|
|
187
187
|
// silent-inert-env trap this helper exists to make checkable). Shared with world-runtime so
|
|
188
188
|
// its "this injectEnv var is inert" warning can never drift from what readMap actually reads.
|
|
189
|
-
function twinEnvStem(vendor) {
|
|
190
|
-
return String(vendor).toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
191
|
-
}
|
|
192
189
|
|
|
193
190
|
function twinUrlVendor(name) {
|
|
194
191
|
const match = /^([A-Z][A-Z0-9_]*)_TWIN_URL$/.exec(String(name || ''));
|
|
@@ -200,20 +197,7 @@ function readMap(env) {
|
|
|
200
197
|
const map = {};
|
|
201
198
|
// the World instance whose recorded app hosts this map routes (APP HOSTS, below); not a vendor, so not a key
|
|
202
199
|
if (env.VOLTER_WORLD_INSTANCE) Object.defineProperty(map, APP_INSTANCE, { value: env.VOLTER_WORLD_INSTANCE, enumerable: false });
|
|
203
|
-
|
|
204
|
-
const url = env[`${twinEnvStem(vendor)}_TWIN_URL`];
|
|
205
|
-
if (url) map[vendor] = url.replace(/\/$/, '');
|
|
206
|
-
}
|
|
207
|
-
if (env.TWIN_INJECT) {
|
|
208
|
-
for (const pair of env.TWIN_INJECT.split(',')) {
|
|
209
|
-
const eq = pair.indexOf('=');
|
|
210
|
-
if (eq <= 0) continue;
|
|
211
|
-
const vendor = pair.slice(0, eq).trim();
|
|
212
|
-
const url = pair.slice(eq + 1).trim();
|
|
213
|
-
if (vendor && url && VENDOR_HOSTS[vendor]) map[vendor] = url.replace(/\/$/, '');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return map;
|
|
200
|
+
return Object.assign(map, twinOrigins(env));
|
|
217
201
|
}
|
|
218
202
|
|
|
219
203
|
/**
|
|
@@ -237,11 +221,12 @@ function resolveTwin(hostname, map, pathname) {
|
|
|
237
221
|
}
|
|
238
222
|
// last, the application's own production hostnames, routed to where it listens in the World; never the World's own
|
|
239
223
|
// origin or a twin's, whose requests carry the World key
|
|
240
|
-
const app = appRoute(map[APP_INSTANCE]);
|
|
241
224
|
const bare = host.replace(/\.$/, '');
|
|
242
225
|
const base = worldBase();
|
|
243
|
-
// a name a vendor's host rule gives
|
|
244
|
-
|
|
226
|
+
// a name a vendor's host rule gives a twin running here is that twin's, never an application's (a vendor with no
|
|
227
|
+
// twin here can be one of the World's applications: the product itself)
|
|
228
|
+
const app = appRouteFor(map[APP_INSTANCE], bare, map);
|
|
229
|
+
if (app && !isTwinOriginHost(bare) && (!base || new URL(base.origin).hostname !== bare)) {
|
|
245
230
|
return { vendor: 'app', origin: app.origin };
|
|
246
231
|
}
|
|
247
232
|
return null;
|
|
@@ -254,7 +239,7 @@ function resolveTwin(hostname, map, pathname) {
|
|
|
254
239
|
// was made with (appForwardHeaders), so an application serving several hostnames answers each as in production.
|
|
255
240
|
const APP_INSTANCE = Symbol.for('volter.inject.appInstance');
|
|
256
241
|
// the route itself (the record, its wildcards, the forwarded headers) has one home, shared with the twins' deliveries
|
|
257
|
-
const {
|
|
242
|
+
const { appRouteFor, appForwardHeaders, appRequestOptions, sessionCa } = require('./app-route.cjs');
|
|
258
243
|
|
|
259
244
|
// HOSTS A TWIN CLAIMS WHILE IT RUNS (packRegistry `hostsClaimed`): no descriptor can list a name a person makes answer
|
|
260
245
|
// to a vendor, so each claiming twin lists its own at its door, read here every five seconds from the moment the
|
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');
|
package/vendor-hosts.cjs
CHANGED
|
@@ -198,4 +198,36 @@ function isVendorHost(host) {
|
|
|
198
198
|
return Object.keys(VENDOR_HOSTS).some((vendor) => VENDOR_HOSTS[vendor](h));
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
/** The vendors whose host rule names `host` (any path): the twins that answer it while they run. */
|
|
202
|
+
function vendorsOfHost(host) {
|
|
203
|
+
const h = String(host || '').toLowerCase().replace(/\.$/, '');
|
|
204
|
+
return Object.keys(VENDOR_HOSTS).filter((vendor) => VENDOR_HOSTS[vendor](h));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A vendor key's env stem: `S3_TWIN_URL` is s3's; `googleauth` → GOOGLEAUTH. */
|
|
208
|
+
function twinEnvStem(vendor) {
|
|
209
|
+
return String(vendor).toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The vendors whose twin `env` points at, each with its origin: `<STEM>_TWIN_URL`, then `TWIN_INJECT` pairs
|
|
213
|
+
* (`vendor=url,…`). The one reading of it: the injector's map is this, and so is "the twins the World runs" wherever a
|
|
214
|
+
* route asks it (a service's id is not its vendor key: the aws service serves s3, dynamodb and others). */
|
|
215
|
+
function twinOrigins(env) {
|
|
216
|
+
const map = {};
|
|
217
|
+
for (const vendor of Object.keys(VENDOR_HOSTS)) {
|
|
218
|
+
const url = env[`${twinEnvStem(vendor)}_TWIN_URL`];
|
|
219
|
+
if (url) map[vendor] = url.replace(/\/$/, '');
|
|
220
|
+
}
|
|
221
|
+
if (env.TWIN_INJECT) {
|
|
222
|
+
for (const pair of String(env.TWIN_INJECT).split(',')) {
|
|
223
|
+
const eq = pair.indexOf('=');
|
|
224
|
+
if (eq <= 0) continue;
|
|
225
|
+
const vendor = pair.slice(0, eq).trim();
|
|
226
|
+
const url = pair.slice(eq + 1).trim();
|
|
227
|
+
if (vendor && url && Object.hasOwn(VENDOR_HOSTS, vendor)) map[vendor] = url.replace(/\/$/, '');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return map;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = { VENDOR_HOSTS, isVendorHost, vendorsOfHost, twinEnvStem, twinOrigins };
|