@volter/world-core 3.0.30 → 3.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/host-routing.cjs +106 -0
- package/dist/host-routing.d.cts +7 -0
- package/dist/inject.cjs +5 -8
- package/dist/src/board-keys.d.ts +6 -0
- package/dist/src/board-keys.js +4 -0
- package/dist/src/browser-cookie-jar.d.ts +9 -0
- package/dist/src/browser-cookie-jar.js +104 -0
- package/dist/src/browser-state.d.ts +52 -0
- package/dist/src/browser-state.js +105 -0
- package/dist/src/browser.d.ts +12 -0
- package/dist/src/browser.js +197 -0
- package/dist/src/derived-core.d.ts +3 -7
- package/dist/src/derived.d.ts +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/pack-fetch.d.ts +0 -3
- package/dist/src/pack-fetch.js +5 -26
- package/dist/src/public-suffixes.json +1 -0
- package/dist/src/runtime.d.ts +1 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/twin-fetch.d.ts +1 -1
- package/dist/src/twin-fetch.js +23 -4
- package/dist/vendor-hosts.cjs +13 -105
- package/dist/vendor-hosts.d.cts +3 -0
- package/host-routing.cjs +106 -0
- package/host-routing.d.cts +7 -0
- package/inject.cjs +5 -8
- package/package.json +11 -1
- package/src/board-keys.ts +4 -0
- package/src/browser-cookie-jar.ts +77 -0
- package/src/browser-session.ts +1 -1
- package/src/browser-state.ts +89 -0
- package/src/browser.ts +164 -0
- package/src/derived-core.ts +4 -5
- package/src/derived.ts +1 -1
- package/src/index.ts +2 -0
- package/src/pack-fetch.ts +6 -24
- package/src/public-suffixes.json +1 -0
- package/src/runtime.ts +2 -1
- package/src/twin-fetch.ts +9 -4
- package/vendor-hosts.cjs +13 -105
- package/vendor-hosts.d.cts +3 -0
- package/dist/src/browser-session-permit.d.ts +0 -13
- package/dist/src/browser-session-permit.js +0 -51
- package/src/browser-session-permit.ts +0 -43
package/src/runtime.ts
CHANGED
|
@@ -344,4 +344,5 @@ export type { HistoryView, HistoryLayout, HistoryReference, HistoryOrigin } from
|
|
|
344
344
|
export { foldHistory } from './log.ts';
|
|
345
345
|
export { volterHome } from './volter-home.ts';
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
|
|
348
|
+
export { boardFrameKey } from './board-keys.ts';
|
package/src/twin-fetch.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertBrowserName } from './browser-state.ts';
|
|
1
2
|
// THE ONE HTTP ADAPTATION (runtime contract R12b): most packs' serve path is the same
|
|
2
3
|
// ~20 lines of glue — the keyless GET /twin manifest door, the body read, a header map,
|
|
3
4
|
// the dispatch into `handle<Vendor>TwinRequest` with the WORLD instant, and a JSON
|
|
@@ -55,12 +56,14 @@ export const siteLabel = (hostname: string): string => hostname.toLowerCase().re
|
|
|
55
56
|
|
|
56
57
|
/** The address a sites template gives `hostname`, or null where the template is not one or the name makes no address
|
|
57
58
|
* (a label over 63 characters). */
|
|
58
|
-
export function siteUrlOf(template: string, hostname: string): string | null {
|
|
59
|
+
export function siteUrlOf(template: string, hostname: string, browser?: string): string | null {
|
|
59
60
|
const host = hostname.toLowerCase();
|
|
61
|
+
if (browser) { try { assertBrowserName(browser); } catch { return null; } }
|
|
62
|
+
const label = `${browser ? `${browser}--` : ''}${siteLabel(host)}`;
|
|
60
63
|
if (!/^[a-z0-9.-]+$/.test(host)) return null;
|
|
61
64
|
// a one-label site is never an IDN's (`xn--…` would be read as punycode, and is no address)
|
|
62
|
-
const filled = /^https?:\/\/\{host\}\.[A-Za-z0-9.-]+(:\d+)?$/.test(template) ? template.replace('{host}', host)
|
|
63
|
-
: /^https?:\/\/\{site\}--[A-Za-z0-9.-]+(:\d+)?$/.test(template) && !host.startsWith('xn--') ? template.replace('{site}',
|
|
65
|
+
const filled = /^https?:\/\/\{host\}\.[A-Za-z0-9.-]+(:\d+)?$/.test(template) ? template.replace('{host}', browser ? label : host)
|
|
66
|
+
: /^https?:\/\/\{site\}--[A-Za-z0-9.-]+(:\d+)?$/.test(template) && !host.startsWith('xn--') ? template.replace('{site}', label) : null;
|
|
64
67
|
try { return filled && new URL(filled).hostname.split('.').every((l) => l.length > 0 && l.length <= 63) ? filled : null; } catch { return null; }
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -69,7 +72,9 @@ export function siteUrlOf(template: string, hostname: string): string | null {
|
|
|
69
72
|
* mints its own URLs from twinPublicBase. */
|
|
70
73
|
export function twinSiteUrl(request: Request, hostname: string): string {
|
|
71
74
|
const at = request.headers.get(TWIN_SITES_HEADER);
|
|
72
|
-
|
|
75
|
+
let browser = request.headers.get('x-volter-world-browser') ?? undefined;
|
|
76
|
+
if (browser) { try { assertBrowserName(browser); } catch { browser = undefined; } }
|
|
77
|
+
return (at ? siteUrlOf(at, hostname, browser) : null) ?? `https://${hostname.toLowerCase()}`;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
/** A twin's BYTE-STREAM door: a pack whose clients speak a TCP protocol (SMTP, the MySQL wire) serves
|
package/vendor-hosts.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// THE VENDOR HOST TABLE — which vendor's twin serves a hostname (and, where two vendors share a host, which path), the
|
|
3
|
-
//
|
|
3
|
+
// Node process's loaded facts. The pure compiler and resolver are host-routing.cjs; this loader reads Node facts.
|
|
4
|
+
// The injector (inject.cjs) routes by the table; the
|
|
4
5
|
// application route (app-route.cjs) never answers a name it gives a vendor, in the injector or in a twin's delivery.
|
|
5
|
-
// Moved out of inject.cjs unchanged (2026-09-27).
|
|
6
6
|
// ── PATH-AWARE DISAMBIGUATION (the shared-host case) ─────────────────────────────────────────
|
|
7
7
|
// A predicate is called as `(hostname, pathname)`. `pathname` is OPTIONAL and is `undefined`
|
|
8
8
|
// wherever the caller genuinely does not have one yet — most importantly the proxy's CONNECT
|
|
@@ -25,108 +25,12 @@
|
|
|
25
25
|
// `browserassets` and `googlefavicon`; kernel packages carry no pack descriptor). Every vendor
|
|
26
26
|
// pack's hosts live on its descriptor as DATA — host | suffix | hostPattern, pathPattern,
|
|
27
27
|
// exclude, key — compiled from pack-facts.json.
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
h === 'connect.facebook.net' ||
|
|
35
|
-
h === 'snap.licdn.com' ||
|
|
36
|
-
h === 'px.ads.linkedin.com' ||
|
|
37
|
-
h === 'static.hotjar.com' ||
|
|
38
|
-
h === 'fonts.googleapis.com' ||
|
|
39
|
-
h === 'fonts.gstatic.com' ||
|
|
40
|
-
h === 'api.fontshare.com' ||
|
|
41
|
-
h === 'images.subscribe.dev' ||
|
|
42
|
-
h === 'www.youtube.com' ||
|
|
43
|
-
h === 'www.youtube-nocookie.com' ||
|
|
44
|
-
h === 'i.ytimg.com',
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// descriptor-first migration (architecture.md, "The descriptor"): packs now declare their hosts as DATA on their descriptor
|
|
48
|
-
// (`hosts` on TwinPack), compiled into the committed pack-facts artifact — a plain JSON this
|
|
49
|
-
// preloaded, dependency-free module can `require` natively. The hand table above shrinks toward
|
|
50
|
-
// empty as entries move; a vendor present in both homes throws (dual declaration is the drift
|
|
51
|
-
// this migration ends, and a broken checkout should refuse to inject rather than half-route).
|
|
52
|
-
/** The keys each pack's compiled rules answer under, by vendor: a pack's own facts learned later (addPackHosts) replace
|
|
53
|
-
* that vendor's rules, never another's. */
|
|
54
|
-
const COMPILED = new Map();
|
|
55
|
-
/** The hand table's keys, before any pack's rules: a pack installed later replaces its hand entry in place (keeping its
|
|
56
|
-
* position, which decides who answers a shared host first), where the built-in facts may not declare one twice. */
|
|
57
|
-
const HAND = new Set(Object.keys(VENDOR_HOSTS));
|
|
58
|
-
/** One host rule as a predicate over (hostname, pathname?): its one selector (host | suffix | hostPattern) and its
|
|
59
|
-
* pathPattern, a path left out matching any. */
|
|
60
|
-
function compileHostRule(rule, where) {
|
|
61
|
-
const selectors = ['host', 'suffix', 'hostPattern'].filter((k) => rule[k] !== undefined).length;
|
|
62
|
-
if (selectors !== 1) throw new Error(`inject: ${where} rule ${JSON.stringify(rule)} — exactly one of host | suffix | hostPattern (pack-facts.json is hand-edited or stale)`);
|
|
63
|
-
const hostRe = rule.hostPattern === undefined ? null : new RegExp(rule.hostPattern);
|
|
64
|
-
const pathRe = rule.pathPattern === undefined ? null : new RegExp(rule.pathPattern);
|
|
65
|
-
return (h, p) =>
|
|
66
|
-
(rule.host !== undefined ? h === rule.host : rule.suffix !== undefined ? h.endsWith(rule.suffix) : hostRe.test(h))
|
|
67
|
-
&& (pathRe === null || p === undefined || pathRe.test(p));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** A pack's host rules (its descriptor's `hosts`) read as the injector reads them: `names(host)`, whether any include
|
|
71
|
-
* rule's selector names the host, whatever the path; `takes(host, path)`, whether the request is the pack's (under
|
|
72
|
-
* one of its keys, an include matches and no exclude does). The content route at a World's place holds a
|
|
73
|
-
* `<base>/@<host>/<path>` to it (pack-fetch.ts). */
|
|
74
|
-
function hostRules(rules) {
|
|
75
|
-
const byKey = new Map();
|
|
76
|
-
for (const rule of rules || []) {
|
|
77
|
-
const key = rule.key === undefined ? '' : rule.key;
|
|
78
|
-
if (!byKey.has(key)) byKey.set(key, []);
|
|
79
|
-
byKey.get(key).push(rule);
|
|
80
|
-
}
|
|
81
|
-
const keys = [...byKey.values()].map((keyRules) => ({
|
|
82
|
-
includes: keyRules.filter((r) => r.exclude !== true).map((r) => compileHostRule(r, 'a pack')),
|
|
83
|
-
excludes: keyRules.filter((r) => r.exclude === true).map((r) => compileHostRule(r, 'a pack')),
|
|
84
|
-
}));
|
|
85
|
-
return {
|
|
86
|
-
names: (h) => keys.some((k) => k.includes.some((m) => m(h))),
|
|
87
|
-
takes: (h, p) => keys.some((k) => k.includes.some((m) => m(h, p)) && !k.excludes.some((m) => m(h, p))),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function compilePackHosts(packs, overlay) {
|
|
92
|
-
for (const vendor of Object.keys(packs)) {
|
|
93
|
-
const rules = packs[vendor].hosts;
|
|
94
|
-
if (!rules || rules.length === 0) continue;
|
|
95
|
-
for (const key of COMPILED.get(vendor) || []) delete VENDOR_HOSTS[key];
|
|
96
|
-
COMPILED.delete(vendor);
|
|
97
|
-
if (VENDOR_HOSTS[vendor] && !(overlay && HAND.has(vendor))) {
|
|
98
|
-
throw new Error(`inject: vendor "${vendor}" declares hosts on its pack descriptor AND in the hand VENDOR_HOSTS table — one home per fact; delete the hand entry.`);
|
|
99
|
-
}
|
|
100
|
-
// Rules group by `key` (default: the vendor id) — aws's descriptor declares the s3 and
|
|
101
|
-
// secretsmanager routing identities its twin answers under. A key matches when ANY include
|
|
102
|
-
// rule matches AND NO exclude rule matches.
|
|
103
|
-
const byKey = new Map();
|
|
104
|
-
for (const rule of rules) {
|
|
105
|
-
const key = rule.key === undefined ? vendor : rule.key;
|
|
106
|
-
if (!byKey.has(key)) byKey.set(key, []);
|
|
107
|
-
byKey.get(key).push(rule);
|
|
108
|
-
}
|
|
109
|
-
for (const [key, keyRules] of byKey) {
|
|
110
|
-
if (VENDOR_HOSTS[key] && !(overlay && HAND.has(key))) {
|
|
111
|
-
throw new Error(`inject: key "${key}" (pack ${vendor}) is declared twice — on this descriptor and in the hand VENDOR_HOSTS table or another pack's descriptor; a key has one home.`);
|
|
112
|
-
}
|
|
113
|
-
const compile = (rule) => compileHostRule(rule, `key "${key}" (pack ${vendor})`);
|
|
114
|
-
const includes = keyRules.filter((r) => r.exclude !== true).map(compile);
|
|
115
|
-
const excludes = keyRules.filter((r) => r.exclude === true).map(compile);
|
|
116
|
-
VENDOR_HOSTS[key] = (h, p) => includes.some((m) => m(h, p)) && !excludes.some((m) => m(h, p));
|
|
117
|
-
COMPILED.set(vendor, [...(COMPILED.get(vendor) || []), key]);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
{
|
|
122
|
-
const facts = require('./pack-facts.cjs').packFacts();
|
|
123
|
-
const installed = new Set(facts.installed || []);
|
|
124
|
-
compilePackHosts(Object.fromEntries(Object.entries(facts.packs).filter(([vendor]) => !installed.has(vendor))), false);
|
|
125
|
-
compilePackHosts(Object.fromEntries(Object.entries(facts.packs).filter(([vendor]) => installed.has(vendor))), true);
|
|
126
|
-
}
|
|
127
|
-
/** Rules from packs' facts learned after this module loaded (a runtime told the facts of the World it boots): each
|
|
128
|
-
* vendor's replace its earlier rules. */
|
|
129
|
-
function addPackHosts(packs) { compilePackHosts(packs || {}, true); }
|
|
28
|
+
const routing = require('./host-routing.cjs');
|
|
29
|
+
const facts = require('./pack-facts.cjs').packFacts();
|
|
30
|
+
const table = routing.createHostTable(facts.packs, facts.installed);
|
|
31
|
+
const VENDOR_HOSTS = table.hosts;
|
|
32
|
+
const hostRules = routing.hostRules;
|
|
33
|
+
function addPackHosts(packs) { table.addPackHosts(packs); }
|
|
130
34
|
|
|
131
35
|
/** Whether any vendor's host rule names `host` (at the host level, whatever the path), whether or not its twin runs. */
|
|
132
36
|
function isVendorHost(host) {
|
|
@@ -166,4 +70,8 @@ function twinOrigins(env) {
|
|
|
166
70
|
return map;
|
|
167
71
|
}
|
|
168
72
|
|
|
169
|
-
|
|
73
|
+
/** Resolve using this Node process's loaded facts. */
|
|
74
|
+
function resolveTwinHost(hostname, map, pathname, claimed) {
|
|
75
|
+
return routing.resolveTwinHost(VENDOR_HOSTS, hostname, map, pathname, claimed);
|
|
76
|
+
}
|
|
77
|
+
module.exports = { resolveTwinHost, VENDOR_HOSTS, hostRules, addPackHosts, isVendorHost, vendorsOfHost, twinEnvStem, twinOrigins };
|
package/vendor-hosts.d.cts
CHANGED
|
@@ -9,3 +9,6 @@ export function twinOrigins(env: Readonly<Record<string, string | undefined>>):
|
|
|
9
9
|
/** A pack's host rules (its descriptor's `hosts`, as data) read as the injector reads them: whether any include names a
|
|
10
10
|
* host whatever the path, and whether a request to a host and path is the pack's. */
|
|
11
11
|
export function hostRules(rules: ReadonlyArray<{ host?: string; suffix?: string; hostPattern?: string; pathPattern?: string; key?: string; exclude?: true }> | undefined): { names(hostname: string): boolean; takes(hostname: string, pathname: string): boolean };
|
|
12
|
+
|
|
13
|
+
/** Resolve an active twin exactly as the proxy, first declared host/path rules, then claimed hosts. */
|
|
14
|
+
export function resolveTwinHost(hostname: string, map: Readonly<Record<string, string>>, pathname?: string, claimed?: Readonly<Record<string, ReadonlySet<string> | (() => ReadonlySet<string>)>>): { vendor: string; origin: string } | null;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export declare const BROWSER_SESSION_HEADER = "x-volter-browser-session-permit";
|
|
2
|
-
export declare const BROWSER_SESSION_PATH = "/_twin/browser-session";
|
|
3
|
-
export type BrowserSessionChoice = {
|
|
4
|
-
person: string | null;
|
|
5
|
-
land: string;
|
|
6
|
-
until: number;
|
|
7
|
-
};
|
|
8
|
-
/** No inbound route can supply the host's forwarding permit. Preserve streamed request bodies. */
|
|
9
|
-
export declare function stripBrowserSessionPermit(request: Request): Request;
|
|
10
|
-
/** Forward once with a secret permit; even a failed forward leaves no live permit behind. */
|
|
11
|
-
export declare function withBrowserSessionPermit<T>(service: string, root: string, choice: BrowserSessionChoice, forward: (permit: string) => Promise<T>): Promise<T>;
|
|
12
|
-
/** The twin takes the host's choice once, under the store's cross-process lock. No header alone grants it. */
|
|
13
|
-
export declare function takeBrowserSessionPermit(request: Request, service: string, root?: string): BrowserSessionChoice | null;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// A host-only permission to change a browser session. Kept outside the twin's log and never sent to a browser.
|
|
2
|
-
import { withHeaders } from "./with-headers.js";
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { getActiveWorldStore } from "./world-store.js";
|
|
5
|
-
import { worldPaths } from "./storage.js";
|
|
6
|
-
export const BROWSER_SESSION_HEADER = 'x-volter-browser-session-permit';
|
|
7
|
-
export const BROWSER_SESSION_PATH = '/_twin/browser-session';
|
|
8
|
-
const directory = (service, root) => join(worldPaths(service, root).dir, 'browser-session-permits');
|
|
9
|
-
/** No inbound route can supply the host's forwarding permit. Preserve streamed request bodies. */
|
|
10
|
-
export function stripBrowserSessionPermit(request) {
|
|
11
|
-
if (!request.headers.has(BROWSER_SESSION_HEADER))
|
|
12
|
-
return request;
|
|
13
|
-
const headers = new Headers(request.headers);
|
|
14
|
-
headers.delete(BROWSER_SESSION_HEADER);
|
|
15
|
-
return withHeaders(request, headers);
|
|
16
|
-
}
|
|
17
|
-
/** Forward once with a secret permit; even a failed forward leaves no live permit behind. */
|
|
18
|
-
export async function withBrowserSessionPermit(service, root, choice, forward) {
|
|
19
|
-
const store = getActiveWorldStore();
|
|
20
|
-
const permit = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString('hex');
|
|
21
|
-
const path = join(directory(service, root), permit);
|
|
22
|
-
store.write(path, JSON.stringify(choice), { secret: true });
|
|
23
|
-
try {
|
|
24
|
-
return await forward(permit);
|
|
25
|
-
}
|
|
26
|
-
finally {
|
|
27
|
-
store.remove(path);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
/** The twin takes the host's choice once, under the store's cross-process lock. No header alone grants it. */
|
|
31
|
-
export function takeBrowserSessionPermit(request, service, root) {
|
|
32
|
-
const permit = request.headers.get(BROWSER_SESSION_HEADER);
|
|
33
|
-
if (!permit || !/^[0-9a-f]{64}$/.test(permit))
|
|
34
|
-
return null;
|
|
35
|
-
const store = getActiveWorldStore();
|
|
36
|
-
const dir = directory(service, root);
|
|
37
|
-
return store.withLock(`${dir}.lock`, () => {
|
|
38
|
-
const path = join(dir, permit);
|
|
39
|
-
const held = store.read(path);
|
|
40
|
-
if (held === null)
|
|
41
|
-
return null;
|
|
42
|
-
store.remove(path);
|
|
43
|
-
try {
|
|
44
|
-
const choice = JSON.parse(held);
|
|
45
|
-
return choice.until > Date.now() && (choice.person === null || typeof choice.person === 'string') && typeof choice.land === 'string' ? choice : null;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// A host-only permission to change a browser session. Kept outside the twin's log and never sent to a browser.
|
|
2
|
-
import { withHeaders } from './with-headers.ts';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { getActiveWorldStore } from './world-store.ts';
|
|
5
|
-
import { worldPaths } from './storage.ts';
|
|
6
|
-
|
|
7
|
-
export const BROWSER_SESSION_HEADER = 'x-volter-browser-session-permit';
|
|
8
|
-
export const BROWSER_SESSION_PATH = '/_twin/browser-session';
|
|
9
|
-
export type BrowserSessionChoice = { person: string | null; land: string; until: number };
|
|
10
|
-
const directory = (service: string, root?: string): string => join(worldPaths(service, root).dir, 'browser-session-permits');
|
|
11
|
-
|
|
12
|
-
/** No inbound route can supply the host's forwarding permit. Preserve streamed request bodies. */
|
|
13
|
-
export function stripBrowserSessionPermit(request: Request): Request {
|
|
14
|
-
if (!request.headers.has(BROWSER_SESSION_HEADER)) return request;
|
|
15
|
-
const headers = new Headers(request.headers);
|
|
16
|
-
headers.delete(BROWSER_SESSION_HEADER);
|
|
17
|
-
return withHeaders(request, headers);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Forward once with a secret permit; even a failed forward leaves no live permit behind. */
|
|
21
|
-
export async function withBrowserSessionPermit<T>(service: string, root: string, choice: BrowserSessionChoice, forward: (permit: string) => Promise<T>): Promise<T> {
|
|
22
|
-
const store = getActiveWorldStore();
|
|
23
|
-
const permit = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString('hex');
|
|
24
|
-
const path = join(directory(service, root), permit);
|
|
25
|
-
store.write(path, JSON.stringify(choice), { secret: true });
|
|
26
|
-
try { return await forward(permit); } finally { store.remove(path); }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The twin takes the host's choice once, under the store's cross-process lock. No header alone grants it. */
|
|
30
|
-
export function takeBrowserSessionPermit(request: Request, service: string, root?: string): BrowserSessionChoice | null {
|
|
31
|
-
const permit = request.headers.get(BROWSER_SESSION_HEADER);
|
|
32
|
-
if (!permit || !/^[0-9a-f]{64}$/.test(permit)) return null;
|
|
33
|
-
const store = getActiveWorldStore(); const dir = directory(service, root);
|
|
34
|
-
return store.withLock(`${dir}.lock`, () => {
|
|
35
|
-
const path = join(dir, permit); const held = store.read(path);
|
|
36
|
-
if (held === null) return null;
|
|
37
|
-
store.remove(path);
|
|
38
|
-
try {
|
|
39
|
-
const choice = JSON.parse(held) as BrowserSessionChoice;
|
|
40
|
-
return choice.until > Date.now() && (choice.person === null || typeof choice.person === 'string') && typeof choice.land === 'string' ? choice : null;
|
|
41
|
-
} catch { return null; }
|
|
42
|
-
});
|
|
43
|
-
}
|