@crouton-kit/crouter 0.3.34 → 0.3.35
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/builtin-pi-packages/pi-mode-switch/extensions/index.ts +10 -5
- package/dist/clients/attach/attach-cmd.js +732 -704
- package/dist/core/__tests__/fixtures/fake-engine.js +18 -8
- package/dist/core/__tests__/flagship-lifecycle.test.js +4 -1
- package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +1 -1
- package/dist/core/__tests__/full/detach-focus.test.js +15 -1
- package/dist/core/__tests__/live-mutation-verbs.test.js +8 -2
- package/dist/core/__tests__/live-mutation.test.js +7 -2
- package/dist/core/hearth/config.js +3 -6
- package/dist/core/hearth/index.d.ts +0 -1
- package/dist/core/hearth/index.js +0 -1
- package/dist/core/hearth/model-auth-guest.js +9 -14
- package/dist/core/hearth/providers/blaxel.js +3 -3
- package/dist/core/hearth/types.d.ts +0 -1
- package/dist/web-client/assets/index-DUThOUzU.css +2 -0
- package/dist/web-client/assets/index-IAJVtuVe.js +80 -0
- package/dist/web-client/index.html +2 -2
- package/package.json +2 -3
- package/dist/core/hearth/registry.d.ts +0 -15
- package/dist/core/hearth/registry.js +0 -180
- package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +0 -289
- package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +0 -34
- package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +0 -203
- package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/hardening.test.js +0 -59
- package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +0 -372
- package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +0 -258
- package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +0 -437
- package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +0 -15
- package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +0 -141
- package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +0 -1
- package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +0 -143
- package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +0 -58
- package/dist/hearth/wake-proxy/anthropic-oauth.js +0 -189
- package/dist/hearth/wake-proxy/auth.d.ts +0 -22
- package/dist/hearth/wake-proxy/auth.js +0 -128
- package/dist/hearth/wake-proxy/config.d.ts +0 -2
- package/dist/hearth/wake-proxy/config.js +0 -151
- package/dist/hearth/wake-proxy/guest-source.d.ts +0 -14
- package/dist/hearth/wake-proxy/guest-source.js +0 -130
- package/dist/hearth/wake-proxy/hearth-status.d.ts +0 -44
- package/dist/hearth/wake-proxy/hearth-status.js +0 -267
- package/dist/hearth/wake-proxy/home.d.ts +0 -61
- package/dist/hearth/wake-proxy/home.js +0 -333
- package/dist/hearth/wake-proxy/main.d.ts +0 -1
- package/dist/hearth/wake-proxy/main.js +0 -134
- package/dist/hearth/wake-proxy/model-auth.d.ts +0 -13
- package/dist/hearth/wake-proxy/model-auth.js +0 -345
- package/dist/hearth/wake-proxy/proxy.d.ts +0 -35
- package/dist/hearth/wake-proxy/proxy.js +0 -716
- package/dist/hearth/wake-proxy/public-source-gate.d.ts +0 -9
- package/dist/hearth/wake-proxy/public-source-gate.js +0 -409
- package/dist/hearth/wake-proxy/redact.d.ts +0 -1
- package/dist/hearth/wake-proxy/redact.js +0 -17
- package/dist/hearth/wake-proxy/server.d.ts +0 -11
- package/dist/hearth/wake-proxy/server.js +0 -142
- package/dist/hearth/wake-proxy/state.d.ts +0 -18
- package/dist/hearth/wake-proxy/state.js +0 -342
- package/dist/hearth/wake-proxy/types.d.ts +0 -76
- package/dist/hearth/wake-proxy/types.js +0 -1
- package/dist/web-client/assets/index-BRKxe-hy.js +0 -80
- package/dist/web-client/assets/index-BZUxTkv5.css +0 -2
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { general } from '../../core/errors.js';
|
|
5
|
-
const ENV = {
|
|
6
|
-
host: 'HEARTH_PROXY_HOST',
|
|
7
|
-
port: 'HEARTH_PROXY_PORT',
|
|
8
|
-
publicOrigin: 'HEARTH_PUBLIC_ORIGIN',
|
|
9
|
-
statePath: 'HEARTH_STATE_PATH',
|
|
10
|
-
m0RegistryPath: 'CRTR_HEARTH_M0_REGISTRY',
|
|
11
|
-
ownerToken: 'HEARTH_OWNER_TOKEN',
|
|
12
|
-
ownerTokenHash: 'HEARTH_OWNER_TOKEN_SHA256',
|
|
13
|
-
ownerUserId: 'HEARTH_OWNER_USER_ID',
|
|
14
|
-
defaultTenantId: 'HEARTH_DEFAULT_TENANT_ID',
|
|
15
|
-
idlePauseMs: 'HEARTH_IDLE_PAUSE_MS',
|
|
16
|
-
wakeTimeoutMs: 'HEARTH_WAKE_TIMEOUT_MS',
|
|
17
|
-
readyTimeoutMs: 'HEARTH_READY_TIMEOUT_MS',
|
|
18
|
-
seedTenantId: 'HEARTH_SEED_TENANT_ID',
|
|
19
|
-
seedMachineId: 'HEARTH_SEED_MACHINE_ID',
|
|
20
|
-
seedNodeId: 'HEARTH_SEED_NODE_ID',
|
|
21
|
-
seedRelayToken: 'HEARTH_SEED_RELAY_TOKEN',
|
|
22
|
-
seedVolumeId: 'HEARTH_SEED_VOLUME_ID',
|
|
23
|
-
seedState: 'HEARTH_SEED_STATE',
|
|
24
|
-
seedWebRoute: 'HEARTH_SEED_WEB_ROUTE',
|
|
25
|
-
};
|
|
26
|
-
function expandHomePath(path) {
|
|
27
|
-
if (path.startsWith('~'))
|
|
28
|
-
return join(homedir(), path.slice(1));
|
|
29
|
-
return resolve(path);
|
|
30
|
-
}
|
|
31
|
-
function parsePort(value, fallback, name) {
|
|
32
|
-
if (value === undefined || value.trim() === '')
|
|
33
|
-
return fallback;
|
|
34
|
-
const parsed = Number(value);
|
|
35
|
-
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
36
|
-
throw general(`invalid ${name}: ${value}`, { env: name });
|
|
37
|
-
}
|
|
38
|
-
return parsed;
|
|
39
|
-
}
|
|
40
|
-
function parseDuration(value, fallback, name) {
|
|
41
|
-
if (value === undefined || value.trim() === '')
|
|
42
|
-
return fallback;
|
|
43
|
-
const parsed = Number(value);
|
|
44
|
-
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
45
|
-
throw general(`invalid ${name}: ${value}`, { env: name });
|
|
46
|
-
}
|
|
47
|
-
return parsed;
|
|
48
|
-
}
|
|
49
|
-
function parseRequiredString(env, name) {
|
|
50
|
-
const value = env[name]?.trim() ?? '';
|
|
51
|
-
if (value === '')
|
|
52
|
-
throw general(`${name} is required`, { env: name });
|
|
53
|
-
return value;
|
|
54
|
-
}
|
|
55
|
-
function normalizeSha256Hash(value) {
|
|
56
|
-
const trimmed = value.trim();
|
|
57
|
-
const body = trimmed.startsWith('sha256:') ? trimmed.slice('sha256:'.length) : trimmed;
|
|
58
|
-
if (!/^[0-9a-fA-F]{64}$/.test(body)) {
|
|
59
|
-
throw general(`invalid owner token hash`, { env: ENV.ownerTokenHash });
|
|
60
|
-
}
|
|
61
|
-
return `sha256:${body.toLowerCase()}`;
|
|
62
|
-
}
|
|
63
|
-
function hashPlaintextToken(token) {
|
|
64
|
-
return `sha256:${createHash('sha256').update(token, 'utf8').digest('hex')}`;
|
|
65
|
-
}
|
|
66
|
-
function parsePublicOrigin(env, host, port) {
|
|
67
|
-
const raw = env[ENV.publicOrigin]?.trim() ?? '';
|
|
68
|
-
if (raw !== '') {
|
|
69
|
-
const origin = new URL(raw).origin;
|
|
70
|
-
return { publicOrigin: origin, cookieSecure: origin.startsWith('https://') };
|
|
71
|
-
}
|
|
72
|
-
if (host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '::ffff:127.0.0.1' || host.startsWith('127.')) {
|
|
73
|
-
const origin = `http://127.0.0.1:${port}`;
|
|
74
|
-
return { publicOrigin: origin, cookieSecure: false };
|
|
75
|
-
}
|
|
76
|
-
throw general(`${ENV.publicOrigin} is required when HEARTH_PROXY_HOST is bound off-loopback`, { env: ENV.publicOrigin, host });
|
|
77
|
-
}
|
|
78
|
-
function parseOwnerHash(env) {
|
|
79
|
-
const plaintext = env[ENV.ownerToken]?.trim() ?? '';
|
|
80
|
-
const hash = env[ENV.ownerTokenHash]?.trim() ?? '';
|
|
81
|
-
if (plaintext === '' && hash === '') {
|
|
82
|
-
throw general(`${ENV.ownerToken} or ${ENV.ownerTokenHash} is required`, { env: ENV.ownerToken });
|
|
83
|
-
}
|
|
84
|
-
if (plaintext !== '' && hash !== '') {
|
|
85
|
-
const derived = hashPlaintextToken(plaintext);
|
|
86
|
-
const normalized = normalizeSha256Hash(hash);
|
|
87
|
-
if (derived !== normalized) {
|
|
88
|
-
throw general(`owner token and owner token hash do not match`, { env: ENV.ownerTokenHash });
|
|
89
|
-
}
|
|
90
|
-
return normalized;
|
|
91
|
-
}
|
|
92
|
-
if (hash !== '')
|
|
93
|
-
return normalizeSha256Hash(hash);
|
|
94
|
-
return hashPlaintextToken(plaintext);
|
|
95
|
-
}
|
|
96
|
-
function parseSeedHome(env, defaultTenantId) {
|
|
97
|
-
const machineId = env[ENV.seedMachineId]?.trim() ?? '';
|
|
98
|
-
const nodeId = env[ENV.seedNodeId]?.trim() ?? '';
|
|
99
|
-
const relayToken = env[ENV.seedRelayToken]?.trim() ?? '';
|
|
100
|
-
const volumeId = env[ENV.seedVolumeId]?.trim() ?? '';
|
|
101
|
-
const tenantId = env[ENV.seedTenantId]?.trim() || defaultTenantId;
|
|
102
|
-
const anyPresent = machineId !== '' || nodeId !== '' || relayToken !== '' || volumeId !== '' || (env[ENV.seedState]?.trim() ?? '') !== '' || (env[ENV.seedWebRoute]?.trim() ?? '') !== '';
|
|
103
|
-
if (!anyPresent)
|
|
104
|
-
return null;
|
|
105
|
-
if (machineId === '' || nodeId === '' || relayToken === '' || volumeId === '') {
|
|
106
|
-
throw general(`HEARTH_SEED_MACHINE_ID, HEARTH_SEED_NODE_ID, HEARTH_SEED_RELAY_TOKEN, and HEARTH_SEED_VOLUME_ID are required together when seeding a new home — run the existing M0 provision/bootstrap/relay flow first`, {
|
|
107
|
-
envs: [ENV.seedMachineId, ENV.seedNodeId, ENV.seedRelayToken, ENV.seedVolumeId],
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
const seedState = (env[ENV.seedState]?.trim() || 'running');
|
|
111
|
-
const webRouteUrl = env[ENV.seedWebRoute]?.trim() ?? '';
|
|
112
|
-
return {
|
|
113
|
-
tenantId,
|
|
114
|
-
machineId,
|
|
115
|
-
nodeId,
|
|
116
|
-
relayToken,
|
|
117
|
-
volumeId,
|
|
118
|
-
state: seedState,
|
|
119
|
-
webRouteUrl: webRouteUrl === '' ? null : webRouteUrl,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
export function loadHearthProxyConfig(env = process.env) {
|
|
123
|
-
const host = (env[ENV.host] ?? '0.0.0.0').trim() || '0.0.0.0';
|
|
124
|
-
const port = parsePort(env[ENV.port], 8080, ENV.port);
|
|
125
|
-
const { publicOrigin, cookieSecure } = parsePublicOrigin(env, host, port);
|
|
126
|
-
const statePath = expandHomePath(env[ENV.statePath] ?? '/data/hearth-proxy.json');
|
|
127
|
-
const m0RegistryPath = expandHomePath(env[ENV.m0RegistryPath] ?? '/data/homes.json');
|
|
128
|
-
const ownerUserId = (env[ENV.ownerUserId] ?? 'silas').trim() || 'silas';
|
|
129
|
-
const defaultTenantFallback = (env[ENV.defaultTenantId] ?? ownerUserId).trim() || ownerUserId;
|
|
130
|
-
const ownerTokenHash = parseOwnerHash(env);
|
|
131
|
-
const idlePauseMs = parseDuration(env[ENV.idlePauseMs], 10 * 60_000, ENV.idlePauseMs);
|
|
132
|
-
const wakeTimeoutMs = parseDuration(env[ENV.wakeTimeoutMs], 150_000, ENV.wakeTimeoutMs);
|
|
133
|
-
const readyTimeoutMs = parseDuration(env[ENV.readyTimeoutMs], 30_000, ENV.readyTimeoutMs);
|
|
134
|
-
const seedHome = parseSeedHome(env, defaultTenantFallback);
|
|
135
|
-
const defaultTenantId = seedHome?.tenantId ?? defaultTenantFallback;
|
|
136
|
-
return {
|
|
137
|
-
host,
|
|
138
|
-
port,
|
|
139
|
-
publicOrigin,
|
|
140
|
-
statePath,
|
|
141
|
-
m0RegistryPath,
|
|
142
|
-
ownerUserId,
|
|
143
|
-
defaultTenantId,
|
|
144
|
-
ownerTokenHash,
|
|
145
|
-
idlePauseMs,
|
|
146
|
-
wakeTimeoutMs,
|
|
147
|
-
readyTimeoutMs,
|
|
148
|
-
cookieSecure,
|
|
149
|
-
seedHome,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { RawResponse, SourceRequest } from '../../core/view/contract.js';
|
|
2
|
-
import type { ActivityHandle, AwakenedHome, TenantActivitySnapshot } from './types.js';
|
|
3
|
-
export interface GuestSourceDeps {
|
|
4
|
-
ensureAwake?: (tenantId: string) => Promise<AwakenedHome>;
|
|
5
|
-
fetch?: typeof fetch;
|
|
6
|
-
acquireActivity?: (tenantId: string) => ActivityHandle;
|
|
7
|
-
releaseActivity?: (handle: ActivityHandle) => TenantActivitySnapshot;
|
|
8
|
-
wakeTimeoutMs?: number;
|
|
9
|
-
bridgeTimeoutMs?: number;
|
|
10
|
-
now?: () => number;
|
|
11
|
-
redact?: (message: string) => string;
|
|
12
|
-
}
|
|
13
|
-
export declare function runGuestSource(tenantId: string, request: SourceRequest, deps?: GuestSourceDeps): Promise<RawResponse>;
|
|
14
|
-
export declare function requireSuccessfulExec<T extends RawResponse>(raw: T, label: string): T;
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { general } from '../../core/errors.js';
|
|
2
|
-
import { acquireActivity as acquireWakeActivity, ensureAwake as ensureTenantAwake, releaseActivity as releaseWakeActivity } from './home.js';
|
|
3
|
-
import { redactWakeProxyMessage } from './redact.js';
|
|
4
|
-
const SECRET_FIELD_JSON_RE = /"(?:access_token|refresh_token|code_verifier|access|refresh|code|state)"\s*:\s*"[^"]*"/gi;
|
|
5
|
-
const SECRET_FIELD_ASSIGN_RE = /\b(?:access_token|refresh_token|code_verifier|access|refresh|code|state)\b\s*[:=]\s*[^\s,&}]+/gi;
|
|
6
|
-
const SECRET_QUERY_RE = /([?&](?:access_token|refresh_token|access|refresh|code|state|code_verifier)=)[^&#\s]+/gi;
|
|
7
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
-
function withTimeout(work, timeoutMs, label, now) {
|
|
9
|
-
if (timeoutMs <= 0)
|
|
10
|
-
return work;
|
|
11
|
-
let timer = null;
|
|
12
|
-
const startedAt = now();
|
|
13
|
-
return Promise.race([
|
|
14
|
-
work,
|
|
15
|
-
new Promise((_, reject) => {
|
|
16
|
-
timer = setTimeout(() => {
|
|
17
|
-
reject(general(`${label} timed out`, { timeoutMs, startedAt: new Date(startedAt).toISOString() }));
|
|
18
|
-
}, timeoutMs);
|
|
19
|
-
timer.unref();
|
|
20
|
-
}),
|
|
21
|
-
]).finally(() => {
|
|
22
|
-
if (timer !== null)
|
|
23
|
-
clearTimeout(timer);
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
function redactGuestSourceMessage(message) {
|
|
27
|
-
return redactWakeProxyMessage(message)
|
|
28
|
-
.replace(SECRET_FIELD_JSON_RE, '"[redacted]"')
|
|
29
|
-
.replace(SECRET_FIELD_ASSIGN_RE, '[redacted]')
|
|
30
|
-
.replace(SECRET_QUERY_RE, '$1[redacted]');
|
|
31
|
-
}
|
|
32
|
-
function normalizeFailure(message, redact) {
|
|
33
|
-
return { ok: false, stdout: '', stderr: redact(message) };
|
|
34
|
-
}
|
|
35
|
-
function parseRawResponse(text) {
|
|
36
|
-
try {
|
|
37
|
-
const parsed = JSON.parse(text);
|
|
38
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
39
|
-
return null;
|
|
40
|
-
if (typeof parsed.ok !== 'boolean' || typeof parsed.stdout !== 'string' || typeof parsed.stderr !== 'string')
|
|
41
|
-
return null;
|
|
42
|
-
if ('exitCode' in parsed && parsed.exitCode !== undefined && typeof parsed.exitCode !== 'number')
|
|
43
|
-
return null;
|
|
44
|
-
if ('status' in parsed && parsed.status !== undefined && typeof parsed.status !== 'number')
|
|
45
|
-
return null;
|
|
46
|
-
return {
|
|
47
|
-
ok: parsed.ok,
|
|
48
|
-
stdout: parsed.stdout,
|
|
49
|
-
stderr: parsed.stderr,
|
|
50
|
-
...(typeof parsed.exitCode === 'number' ? { exitCode: parsed.exitCode } : {}),
|
|
51
|
-
...(typeof parsed.status === 'number' ? { status: parsed.status } : {}),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async function fetchRawResponse(url, request, relayToken, deps) {
|
|
59
|
-
const controller = new AbortController();
|
|
60
|
-
const timeout = setTimeout(() => {
|
|
61
|
-
controller.abort(new Error('guest source bridge timed out'));
|
|
62
|
-
}, deps.bridgeTimeoutMs);
|
|
63
|
-
timeout.unref();
|
|
64
|
-
try {
|
|
65
|
-
const response = await deps.fetch(url, {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: {
|
|
68
|
-
authorization: `Bearer ${relayToken}`,
|
|
69
|
-
'content-type': 'application/json',
|
|
70
|
-
},
|
|
71
|
-
body: JSON.stringify(request),
|
|
72
|
-
signal: controller.signal,
|
|
73
|
-
});
|
|
74
|
-
const body = await response.text();
|
|
75
|
-
if (!response.ok) {
|
|
76
|
-
return normalizeFailure(`guest source bridge returned HTTP ${response.status} ${response.statusText}${body.trim() === '' ? '' : `: ${body}`}`, deps.redact);
|
|
77
|
-
}
|
|
78
|
-
const raw = parseRawResponse(body);
|
|
79
|
-
if (raw === null) {
|
|
80
|
-
return normalizeFailure(`guest source bridge returned invalid RawResponse JSON${body.trim() === '' ? '' : `: ${body}`}`, deps.redact);
|
|
81
|
-
}
|
|
82
|
-
return raw;
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
-
return normalizeFailure(message, deps.redact);
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
clearTimeout(timeout);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
export async function runGuestSource(tenantId, request, deps = {}) {
|
|
93
|
-
const now = deps.now ?? Date.now;
|
|
94
|
-
const redact = deps.redact ?? redactGuestSourceMessage;
|
|
95
|
-
const ensureAwake = deps.ensureAwake ?? ensureTenantAwake;
|
|
96
|
-
const fetchImpl = deps.fetch ?? fetch;
|
|
97
|
-
const acquireActivity = deps.acquireActivity ?? acquireWakeActivity;
|
|
98
|
-
const releaseActivity = deps.releaseActivity ?? releaseWakeActivity;
|
|
99
|
-
const wakeTimeoutMs = deps.wakeTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
100
|
-
const bridgeTimeoutMs = deps.bridgeTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
101
|
-
const activity = acquireActivity(tenantId);
|
|
102
|
-
let awakened = null;
|
|
103
|
-
try {
|
|
104
|
-
awakened = await withTimeout(ensureAwake(tenantId), wakeTimeoutMs, 'guest wake', now);
|
|
105
|
-
const upstream = new URL('/__crtr/source', awakened.routeUrl);
|
|
106
|
-
return await fetchRawResponse(upstream, request, awakened.relayToken, { fetch: fetchImpl, bridgeTimeoutMs, now, redact });
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
-
return normalizeFailure(message, redact);
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
releaseActivity(activity);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
export function requireSuccessfulExec(raw, label) {
|
|
117
|
-
if (!raw.ok) {
|
|
118
|
-
throw general(`${label} failed`, { error: redactGuestSourceMessage(raw.stderr || `${label} transport failed`), exitCode: raw.exitCode ?? null });
|
|
119
|
-
}
|
|
120
|
-
if (typeof raw.exitCode !== 'number') {
|
|
121
|
-
throw general(`${label} failed`, { error: `${label} did not return an exit code`, exitCode: null });
|
|
122
|
-
}
|
|
123
|
-
if (raw.exitCode !== 0) {
|
|
124
|
-
throw general(`${label} failed`, { error: redactGuestSourceMessage(raw.stderr || `${label} exited ${raw.exitCode}`), exitCode: raw.exitCode });
|
|
125
|
-
}
|
|
126
|
-
if (raw.stderr.trim() !== '') {
|
|
127
|
-
throw general(`${label} failed`, { error: redactGuestSourceMessage(raw.stderr), exitCode: raw.exitCode });
|
|
128
|
-
}
|
|
129
|
-
return raw;
|
|
130
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { MachineRef } from '../../core/hearth/types.js';
|
|
2
|
-
import { runGuestSource } from './guest-source.js';
|
|
3
|
-
import type { HearthWakeProxyRuntime } from './types.js';
|
|
4
|
-
export interface HearthMachineResources {
|
|
5
|
-
cpuCount: number | null;
|
|
6
|
-
memoryMB: number | null;
|
|
7
|
-
}
|
|
8
|
-
export interface HearthRuntimeStats {
|
|
9
|
-
cpuPct: number | null;
|
|
10
|
-
memoryUsedMB: number | null;
|
|
11
|
-
memoryAvailableMB: number | null;
|
|
12
|
-
crtrRssKB: number | null;
|
|
13
|
-
piRssKB: number | null;
|
|
14
|
-
nodeRssKB: number | null;
|
|
15
|
-
totalRssKB: number | null;
|
|
16
|
-
capturedAt: string | null;
|
|
17
|
-
staleMs: number | null;
|
|
18
|
-
}
|
|
19
|
-
export interface HearthStatusResponse {
|
|
20
|
-
tenantId: string;
|
|
21
|
-
machineIdSuffix: string | null;
|
|
22
|
-
nodeId: string | null;
|
|
23
|
-
routeHost: string | null;
|
|
24
|
-
homeState: string | null;
|
|
25
|
-
activeCounts: {
|
|
26
|
-
http: number;
|
|
27
|
-
ws: number;
|
|
28
|
-
wake: number;
|
|
29
|
-
};
|
|
30
|
-
activeTotal: number;
|
|
31
|
-
idleDeadlineAt: string | null;
|
|
32
|
-
lastActivityAt: string | null;
|
|
33
|
-
lastWakeAt: string | null;
|
|
34
|
-
lastPauseAt: string | null;
|
|
35
|
-
machineResources: HearthMachineResources | null;
|
|
36
|
-
runtimeStats: HearthRuntimeStats | null;
|
|
37
|
-
}
|
|
38
|
-
export interface HearthStatusDeps {
|
|
39
|
-
getMachine?: (machineId: string) => Promise<MachineRef | null>;
|
|
40
|
-
guestSource?: typeof runGuestSource;
|
|
41
|
-
fetch?: typeof fetch;
|
|
42
|
-
now?: () => number;
|
|
43
|
-
}
|
|
44
|
-
export declare function loadHearthStatus(tenantId: string, runtime: HearthWakeProxyRuntime, deps?: HearthStatusDeps): Promise<HearthStatusResponse>;
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { getHomeBackend } from '../../core/hearth/provider.js';
|
|
2
|
-
import { ensureProxyState, getTenantActivitySnapshot, loadProxyState, readTenantHome } from './state.js';
|
|
3
|
-
const RESOURCE_PROBE_TIMEOUT_MS = 5_000;
|
|
4
|
-
function resourceProbeScript() {
|
|
5
|
-
return `import { readdir, readFile } from 'node:fs/promises';
|
|
6
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
|
-
|
|
8
|
-
function numberOrNull(value) {
|
|
9
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function parseKb(value) {
|
|
13
|
-
if (typeof value !== 'string') return null;
|
|
14
|
-
const match = value.trim().match(/^(\d+)\s+kB$/i);
|
|
15
|
-
return match === null ? null : Number(match[1]);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function parseMemInfo(text) {
|
|
19
|
-
const entries = new Map();
|
|
20
|
-
for (const line of text.split('\n')) {
|
|
21
|
-
const match = line.match(/^([^:]+):\s*(\d+)\s+kB$/i);
|
|
22
|
-
if (match !== null) entries.set(match[1], Number(match[2]));
|
|
23
|
-
}
|
|
24
|
-
const total = entries.get('MemTotal') ?? null;
|
|
25
|
-
const available = entries.get('MemAvailable') ?? entries.get('MemFree') ?? null;
|
|
26
|
-
return {
|
|
27
|
-
memoryUsedMB: total !== null && available !== null ? Math.max(0, Math.round((total - available) / 1024)) : null,
|
|
28
|
-
memoryAvailableMB: available !== null ? Math.max(0, Math.round(available / 1024)) : null,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function sampleCpu() {
|
|
33
|
-
const read = async () => {
|
|
34
|
-
const line = (await readFile('/proc/stat', 'utf8')).split('\n', 1)[0] ?? '';
|
|
35
|
-
const values = line.trim().split(/\s+/).slice(1).map((value) => Number(value));
|
|
36
|
-
if (values.length === 0 || values.some((value) => !Number.isFinite(value))) return null;
|
|
37
|
-
const idle = (values[3] ?? 0) + (values[4] ?? 0);
|
|
38
|
-
const total = values.reduce((sum, value) => sum + value, 0);
|
|
39
|
-
return { idle, total };
|
|
40
|
-
};
|
|
41
|
-
const first = await read();
|
|
42
|
-
if (first === null) return null;
|
|
43
|
-
await sleep(200);
|
|
44
|
-
const second = await read();
|
|
45
|
-
if (second === null) return null;
|
|
46
|
-
const total = second.total - first.total;
|
|
47
|
-
if (total <= 0) return null;
|
|
48
|
-
const idle = second.idle - first.idle;
|
|
49
|
-
return Math.max(0, Math.min(100, Math.round(((total - idle) / total) * 1000) / 10));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function parseVmRss(statusText) {
|
|
53
|
-
const match = statusText.match(/^VmRSS:\s*(\d+)\s*kB$/mi);
|
|
54
|
-
return match === null ? null : Number(match[1]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function classifyProcess(comm, cmdline) {
|
|
58
|
-
const text = comm + '\n' + cmdline;
|
|
59
|
-
if (/\b(?:node_modules\/(?:\.bin\/)?pi|(?:^|\/)pi)(?:\b|$)/i.test(text)) return 'pi';
|
|
60
|
-
if (/\b(?:crtr|dist\/cli\.js|bin\/crtr)(?:\b|$)/i.test(text)) return 'crtr';
|
|
61
|
-
if (/\bnode(?:\.js)?\b/i.test(text)) return 'node';
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function sampleRss() {
|
|
66
|
-
const totals = { crtrRssKB: null, piRssKB: null, nodeRssKB: null, totalRssKB: null };
|
|
67
|
-
let crtr = 0;
|
|
68
|
-
let pi = 0;
|
|
69
|
-
let node = 0;
|
|
70
|
-
try {
|
|
71
|
-
const entries = await readdir('/proc', { withFileTypes: true });
|
|
72
|
-
for (const entry of entries) {
|
|
73
|
-
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
|
|
74
|
-
const pid = entry.name;
|
|
75
|
-
try {
|
|
76
|
-
const [comm, cmdline, status] = await Promise.all([
|
|
77
|
-
readFile('/proc/' + pid + '/comm', 'utf8').catch(() => ''),
|
|
78
|
-
readFile('/proc/' + pid + '/cmdline', 'utf8').catch(() => ''),
|
|
79
|
-
readFile('/proc/' + pid + '/status', 'utf8').catch(() => ''),
|
|
80
|
-
]);
|
|
81
|
-
const rss = parseVmRss(status);
|
|
82
|
-
const kind = classifyProcess(comm.trim(), cmdline.replaceAll('\0', ' ').trim());
|
|
83
|
-
if (rss === null || kind === null) continue;
|
|
84
|
-
if (kind === 'crtr') crtr += rss;
|
|
85
|
-
else if (kind === 'pi') pi += rss;
|
|
86
|
-
else if (kind === 'node') node += rss;
|
|
87
|
-
} catch {
|
|
88
|
-
/* ignore per-process probe failures */
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
return totals;
|
|
93
|
-
}
|
|
94
|
-
totals.crtrRssKB = crtr > 0 ? crtr : null;
|
|
95
|
-
totals.piRssKB = pi > 0 ? pi : null;
|
|
96
|
-
totals.nodeRssKB = node > 0 ? node : null;
|
|
97
|
-
const total = crtr + pi + node;
|
|
98
|
-
totals.totalRssKB = total > 0 ? total : null;
|
|
99
|
-
return totals;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const [cpuPct, mem, rss] = await Promise.all([
|
|
103
|
-
sampleCpu().catch(() => null),
|
|
104
|
-
readFile('/proc/meminfo', 'utf8').then(parseMemInfo).catch(() => ({ memoryUsedMB: null, memoryAvailableMB: null })),
|
|
105
|
-
sampleRss().catch(() => ({ crtrRssKB: null, piRssKB: null, nodeRssKB: null, totalRssKB: null })),
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
|
-
console.log(JSON.stringify({
|
|
109
|
-
cpuPct,
|
|
110
|
-
memoryUsedMB: mem.memoryUsedMB,
|
|
111
|
-
memoryAvailableMB: mem.memoryAvailableMB,
|
|
112
|
-
crtrRssKB: rss.crtrRssKB,
|
|
113
|
-
piRssKB: rss.piRssKB,
|
|
114
|
-
nodeRssKB: rss.nodeRssKB,
|
|
115
|
-
totalRssKB: rss.totalRssKB,
|
|
116
|
-
capturedAt: new Date().toISOString(),
|
|
117
|
-
}));`;
|
|
118
|
-
}
|
|
119
|
-
function parseNumber(value) {
|
|
120
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
121
|
-
}
|
|
122
|
-
function parseRuntimeStats(raw, now) {
|
|
123
|
-
let parsed;
|
|
124
|
-
try {
|
|
125
|
-
parsed = JSON.parse(raw);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
131
|
-
return null;
|
|
132
|
-
const record = parsed;
|
|
133
|
-
const capturedAt = typeof record.capturedAt === 'string' && record.capturedAt.trim() !== '' ? record.capturedAt : null;
|
|
134
|
-
const staleMs = capturedAt === null ? null : Math.max(0, now() - Date.parse(capturedAt));
|
|
135
|
-
const stats = {
|
|
136
|
-
cpuPct: parseNumber(record.cpuPct),
|
|
137
|
-
memoryUsedMB: parseNumber(record.memoryUsedMB),
|
|
138
|
-
memoryAvailableMB: parseNumber(record.memoryAvailableMB),
|
|
139
|
-
crtrRssKB: parseNumber(record.crtrRssKB),
|
|
140
|
-
piRssKB: parseNumber(record.piRssKB),
|
|
141
|
-
nodeRssKB: parseNumber(record.nodeRssKB),
|
|
142
|
-
totalRssKB: parseNumber(record.totalRssKB),
|
|
143
|
-
capturedAt,
|
|
144
|
-
staleMs: Number.isFinite(staleMs) ? staleMs : null,
|
|
145
|
-
};
|
|
146
|
-
return Object.values(stats).every((value) => value === null) ? null : stats;
|
|
147
|
-
}
|
|
148
|
-
function parseRawResponse(raw) {
|
|
149
|
-
try {
|
|
150
|
-
const parsed = JSON.parse(raw);
|
|
151
|
-
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
152
|
-
return null;
|
|
153
|
-
if (typeof parsed.ok !== 'boolean' || typeof parsed.stdout !== 'string' || typeof parsed.stderr !== 'string')
|
|
154
|
-
return null;
|
|
155
|
-
if ('exitCode' in parsed && parsed.exitCode !== undefined && typeof parsed.exitCode !== 'number')
|
|
156
|
-
return null;
|
|
157
|
-
return {
|
|
158
|
-
ok: parsed.ok,
|
|
159
|
-
stdout: parsed.stdout,
|
|
160
|
-
stderr: parsed.stderr,
|
|
161
|
-
...(typeof parsed.exitCode === 'number' ? { exitCode: parsed.exitCode } : {}),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
function runtimeProbeRequest(cwd) {
|
|
169
|
-
return {
|
|
170
|
-
kind: 'exec',
|
|
171
|
-
bin: 'node',
|
|
172
|
-
args: ['--input-type=module', '-e', resourceProbeScript()],
|
|
173
|
-
cwd,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
async function loadMachineResources(runtime, tenantId, deps) {
|
|
177
|
-
const home = readTenantHome(runtime.proxy, tenantId);
|
|
178
|
-
if (home === null)
|
|
179
|
-
return null;
|
|
180
|
-
const getMachine = deps.getMachine ?? (async (machineId) => {
|
|
181
|
-
const backend = getHomeBackend(runtime.m0);
|
|
182
|
-
return await backend.getMachine(machineId);
|
|
183
|
-
});
|
|
184
|
-
try {
|
|
185
|
-
const machine = await getMachine(home.machineId);
|
|
186
|
-
if (machine === null || machine.raw === null || typeof machine.raw !== 'object' || Array.isArray(machine.raw))
|
|
187
|
-
return null;
|
|
188
|
-
const raw = machine.raw;
|
|
189
|
-
const cpuCount = parseNumber(raw.cpuCount);
|
|
190
|
-
const memoryMB = parseNumber(raw.memoryMB);
|
|
191
|
-
return cpuCount === null && memoryMB === null ? null : { cpuCount, memoryMB };
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
async function loadRuntimeStats(runtime, tenantId, deps) {
|
|
198
|
-
const home = readTenantHome(runtime.proxy, tenantId);
|
|
199
|
-
if (home === null || home.state !== 'running')
|
|
200
|
-
return null;
|
|
201
|
-
const routeUrl = home.webRoute?.concreteUrl ?? home.webRoute?.url ?? null;
|
|
202
|
-
const relayToken = home.relayToken ?? null;
|
|
203
|
-
if (routeUrl === null || relayToken === null || routeUrl.trim() === '' || relayToken.trim() === '')
|
|
204
|
-
return null;
|
|
205
|
-
const fetchImpl = deps.fetch ?? fetch;
|
|
206
|
-
const controller = new AbortController();
|
|
207
|
-
const timeout = setTimeout(() => controller.abort(new Error('runtime stats probe timed out')), RESOURCE_PROBE_TIMEOUT_MS);
|
|
208
|
-
timeout.unref();
|
|
209
|
-
try {
|
|
210
|
-
const response = await fetchImpl(new URL('/__crtr/source', routeUrl), {
|
|
211
|
-
method: 'POST',
|
|
212
|
-
headers: {
|
|
213
|
-
authorization: `Bearer ${relayToken}`,
|
|
214
|
-
'content-type': 'application/json',
|
|
215
|
-
},
|
|
216
|
-
body: JSON.stringify(runtimeProbeRequest(runtime.m0.guestCodePath)),
|
|
217
|
-
signal: controller.signal,
|
|
218
|
-
});
|
|
219
|
-
if (!response.ok)
|
|
220
|
-
return null;
|
|
221
|
-
const raw = parseRawResponse(await response.text());
|
|
222
|
-
if (raw === null || !raw.ok || typeof raw.exitCode !== 'number' || raw.exitCode !== 0 || raw.stdout.trim() === '')
|
|
223
|
-
return null;
|
|
224
|
-
return parseRuntimeStats(raw.stdout, deps.now ?? Date.now);
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
finally {
|
|
230
|
-
clearTimeout(timeout);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
export async function loadHearthStatus(tenantId, runtime, deps = {}) {
|
|
234
|
-
const proxyState = loadProxyState(runtime.proxy.statePath) ?? ensureProxyState(runtime.proxy);
|
|
235
|
-
const tenantState = proxyState.tenants[tenantId] ?? null;
|
|
236
|
-
const home = readTenantHome(runtime.proxy, tenantId);
|
|
237
|
-
const activity = getTenantActivitySnapshot(tenantId);
|
|
238
|
-
const [machineResources, runtimeStats] = await Promise.all([
|
|
239
|
-
loadMachineResources(runtime, tenantId, deps),
|
|
240
|
-
loadRuntimeStats(runtime, tenantId, deps),
|
|
241
|
-
]);
|
|
242
|
-
const routeUrl = home?.webRoute?.concreteUrl ?? home?.webRoute?.url ?? null;
|
|
243
|
-
let routeHost = null;
|
|
244
|
-
if (routeUrl !== null) {
|
|
245
|
-
try {
|
|
246
|
-
routeHost = new URL(routeUrl).host;
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
routeHost = null;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
tenantId,
|
|
254
|
-
machineIdSuffix: home?.machineId ? home.machineId.slice(-6) : null,
|
|
255
|
-
nodeId: home?.nodeId ?? null,
|
|
256
|
-
routeHost,
|
|
257
|
-
homeState: home?.state ?? null,
|
|
258
|
-
activeCounts: activity.counts,
|
|
259
|
-
activeTotal: activity.total,
|
|
260
|
-
idleDeadlineAt: activity.idleDeadlineAt,
|
|
261
|
-
lastActivityAt: tenantState?.lastActivityAt ?? null,
|
|
262
|
-
lastWakeAt: tenantState?.lastWakeAt ?? null,
|
|
263
|
-
lastPauseAt: tenantState?.lastPauseAt ?? null,
|
|
264
|
-
machineResources,
|
|
265
|
-
runtimeStats,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { touchTenantWake } from './state.js';
|
|
2
|
-
import type { ActivityHandle, ActivityKind, AwakenedHome, HearthWakeProxyRuntime, TenantActivitySnapshot } from './types.js';
|
|
3
|
-
export declare function configureWakeProxyRuntime(next: HearthWakeProxyRuntime): void;
|
|
4
|
-
declare function probeRelayReady(routeUrl: string, relayToken: string): Promise<boolean>;
|
|
5
|
-
/**
|
|
6
|
-
* Retry the cheap single-shot relay probe until it confirms the route is serving or `windowMs`
|
|
7
|
-
* elapses, returning a boolean. Always runs at least one attempt. This is the reliable answer to
|
|
8
|
-
* "is the relay serving right now?" — a single shot mistakes a slow first connection for a dead
|
|
9
|
-
* route. Both the fast-path short-circuit and `waitForRelayReady` build on it.
|
|
10
|
-
*/
|
|
11
|
-
declare function probeRelayServing(routeUrl: string, relayToken: string, windowMs: number): Promise<boolean>;
|
|
12
|
-
declare function waitForRelayReady(routeUrl: string, relayToken: string): Promise<void>;
|
|
13
|
-
declare function captureAwakeHome(tenantId: string): AwakenedHome;
|
|
14
|
-
/**
|
|
15
|
-
* Cheap, non-throwing route selection for the HTTP fast path: return the persisted home as an
|
|
16
|
-
* `AwakenedHome` ONLY when it is recorded `running` with a usable webRoute + relayToken. Does NO
|
|
17
|
-
* probe, takes NO lock, and never wakes — the actual upstream forward is the liveness check.
|
|
18
|
-
* Returns null when the home is missing, not running, or missing a route/token, so the caller
|
|
19
|
-
* falls back to the deliberate `ensureAwake` wake/restore path. This is the one authoritative
|
|
20
|
-
* place that turns "a known-running home" into a forwardable route without the heavy gate.
|
|
21
|
-
*/
|
|
22
|
-
export declare function resolveRunningHome(tenantId: string): AwakenedHome | null;
|
|
23
|
-
/** Wake the tenant's home through the configured backend seam and persist returned pointers. */
|
|
24
|
-
declare function wakeHome(tenantId: string): Promise<void>;
|
|
25
|
-
export declare const wakeProxyInternals: {
|
|
26
|
-
probeRelayReady: typeof probeRelayReady;
|
|
27
|
-
probeRelayServing: typeof probeRelayServing;
|
|
28
|
-
waitForRelayReady: typeof waitForRelayReady;
|
|
29
|
-
captureAwakeHome: typeof captureAwakeHome;
|
|
30
|
-
resolveRunningHome: typeof resolveRunningHome;
|
|
31
|
-
wakeHome: typeof wakeHome;
|
|
32
|
-
touchTenantWake: typeof touchTenantWake;
|
|
33
|
-
now: () => number;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Drop the cached warm verdict for a tenant so the next `ensureAwake` re-probes (or wakes) instead
|
|
37
|
-
* of trusting a stale "serving" answer. The proxy calls this when an upstream forward fails, so a
|
|
38
|
-
* home that died mid-TTL is re-checked on the next request rather than serving 502s for the rest of
|
|
39
|
-
* the window.
|
|
40
|
-
*/
|
|
41
|
-
export declare function invalidateWarmVerdict(tenantId: string): void;
|
|
42
|
-
/** Clear all warm verdicts and in-flight wake coalescing. Used on runtime (re)configuration. */
|
|
43
|
-
export declare function resetWakeProxyMemory(): void;
|
|
44
|
-
export declare function acquireActivity(tenantId: string, kind?: ActivityKind): ActivityHandle;
|
|
45
|
-
export declare function releaseActivity(handle: ActivityHandle): TenantActivitySnapshot;
|
|
46
|
-
export declare function scheduleIdlePause(tenantId: string): void;
|
|
47
|
-
/**
|
|
48
|
-
* Pause the tenant's home through the backend seam. We never drive a sandbox SDK directly here —
|
|
49
|
-
* the backend returns provider pointer/state changes and this layer persists them. Serialized under the per-tenant
|
|
50
|
-
* in-process lock; the single-process deployment invariant (one wake-proxy machine per tenant)
|
|
51
|
-
* makes this lock authoritative.
|
|
52
|
-
*/
|
|
53
|
-
export declare function pauseNow(tenantId: string): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* Ensure the tenant's home is awake. First probe an already-serving relay; when that succeeds,
|
|
56
|
-
* reconcile the registry state if needed, touch wake, and return the existing route. Otherwise,
|
|
57
|
-
* wake the guest through the backend seam, wait for readiness, touch wake, and return an
|
|
58
|
-
* `AwakenedHome` the proxy can forward to. Serialized under the per-tenant in-process lock.
|
|
59
|
-
*/
|
|
60
|
-
export declare function ensureAwake(tenantId: string): Promise<AwakenedHome>;
|
|
61
|
-
export {};
|