@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 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, route }
18
+ const APP_ROUTES = new Map(); // cache: instance file -> { checkedAt, mtimeMs, routes, services, twins }
19
19
 
20
- /** The application's route in the instance `instanceFile`: { origin, hosts } or null when none is recorded. */
21
- function appRoute(instanceFile) {
22
- if (!instanceFile) return null;
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.route && held.route.origin ? held.route : null;
26
- if (!held) { held = { checkedAt: 0, mtimeMs: -1, route: null }; APP_ROUTES.set(instanceFile, held); }
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
- held.route = Array.isArray(record.hosts) && record.hosts.length
37
- ? { url: typeof record.url === 'string' ? record.url : null, hosts: new Set(record.hosts.map((h) => String(h).toLowerCase())) }
38
- : null;
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.route = null;
63
+ if (error && error.code === 'ENOENT') held.routes = [];
49
64
  }
50
- return held.route && held.route.origin ? held.route : null;
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
- const route = appRoute(env.VOLTER_WORLD_INSTANCE);
74
- const host = url.hostname.toLowerCase().replace(/\.$/, '');
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 = { appRoute, appHostMatch, appForwardHeaders, appDestination, appRequestOptions, deliverToApp, appFetch, sessionCa };
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 appRoute(instanceFile: string | undefined): AppRoute | null;
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;
@@ -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, route }
18
+ const APP_ROUTES = new Map(); // cache: instance file -> { checkedAt, mtimeMs, routes, services, twins }
19
19
 
20
- /** The application's route in the instance `instanceFile`: { origin, hosts } or null when none is recorded. */
21
- function appRoute(instanceFile) {
22
- if (!instanceFile) return null;
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.route && held.route.origin ? held.route : null;
26
- if (!held) { held = { checkedAt: 0, mtimeMs: -1, route: null }; APP_ROUTES.set(instanceFile, held); }
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
- held.route = Array.isArray(record.hosts) && record.hosts.length
37
- ? { url: typeof record.url === 'string' ? record.url : null, hosts: new Set(record.hosts.map((h) => String(h).toLowerCase())) }
38
- : null;
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.route = null;
63
+ if (error && error.code === 'ENOENT') held.routes = [];
49
64
  }
50
- return held.route && held.route.origin ? held.route : null;
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
- const route = appRoute(env.VOLTER_WORLD_INSTANCE);
74
- const host = url.hostname.toLowerCase().replace(/\.$/, '');
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 = { appRoute, appHostMatch, appForwardHeaders, appDestination, appRequestOptions, deliverToApp, appFetch, sessionCa };
268
+ module.exports = { appRouteFor, appHostMatch, appForwardHeaders, appDestination, appRequestOptions, deliverToApp, appFetch, sessionCa };
@@ -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 appRoute(instanceFile: string | undefined): AppRoute | null;
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, isVendorHost } = require('./vendor-hosts.cjs');
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
- for (const vendor of Object.keys(VENDOR_HOSTS)) {
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 its twin is never the application's, whether or not that twin runs here
244
- if (app && appHostMatch(app.hosts, bare) && !isVendorHost(bare) && !isTwinOriginHost(bare) && (!base || new URL(base.origin).hostname !== bare)) {
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 { appRoute, appHostMatch, appForwardHeaders, appRequestOptions, sessionCa } = require('./app-route.cjs');
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
@@ -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
- module.exports = { VENDOR_HOSTS, isVendorHost };
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, isVendorHost } = require('./vendor-hosts.cjs');
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
- for (const vendor of Object.keys(VENDOR_HOSTS)) {
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 its twin is never the application's, whether or not that twin runs here
244
- if (app && appHostMatch(app.hosts, bare) && !isVendorHost(bare) && !isTwinOriginHost(bare) && (!base || new URL(base.origin).hostname !== bare)) {
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 { appRoute, appHostMatch, appForwardHeaders, appRequestOptions, sessionCa } = require('./app-route.cjs');
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.7",
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
- module.exports = { VENDOR_HOSTS, isVendorHost };
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 };