@volter/world-core 2.0.7 → 2.0.8
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/vendor-hosts.cjs +33 -1
- package/inject.cjs +7 -22
- package/package.json +1 -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/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.8",
|
|
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/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 };
|