electron/companion-account-service.mjs
Rama main · réplica de lectura del repositorio original.
import {
ControlPlaneError,
normalizeAccountEmail,
normalizeControlPlaneURL,
} from "./control-plane-client.mjs";
import {
MANAGED_COMPANION_ENDPOINT_FIELD,
MANAGED_COMPANION_RELEASE_AT_FIELD,
managedCompanionTunnelAccess,
normalizeManagedCompanionEndpoint,
withManagedCompanionTunnelAccess,
withoutManagedCompanionTunnelAccess,
} from "./managed-companion-tunnel.mjs";
import { MANAGED_ENDPOINT_PHONES_UNUSED } from "./companion-endpoint-need.mjs";
import { FRIENDLY_MESSAGES, friendlyCompanionAccountError } from "./companion-account-messages.mjs";
export { friendlyCompanionAccountError };
export { managedEndpointNeed, PHONE_UNUSED_RELEASE_MS } from "./companion-endpoint-need.mjs";
export const DEFAULT_COMPANION_CONTROL_PLANE_URL = "https://accounts.openmausbot.com";
export const COMPANION_CLIENT_INSTANCE_FIELD = "companionClientInstanceId";
export const COMPANION_ACCOUNT_TOKEN_FIELD = "companionAccountToken";
export const COMPANION_ACCOUNT_USER_ID_FIELD = "companionAccountUserId";
export const COMPANION_ACCOUNT_EMAIL_FIELD = "companionAccountEmail";
export const COMPANION_INSTALLATION_ID_FIELD = "companionInstallationId";
export const COMPANION_INSTALLATION_CREDENTIAL_FIELD = "companionInstallationCredential";
export const COMPANION_INSTALLATION_EXPIRY_FIELD = "companionInstallationCredentialExpiresAt";
export const COMPANION_ACCOUNT_CLEANUP_PENDING_FIELD = "companionAccountCleanupPending";
export const COMPANION_ENDPOINT_RELEASE_PENDING_FIELD = "companionEndpointReleasePending";
/** The address was given back while phones were paired but unused; the control plane keeps its hostname. */
export const COMPANION_ENDPOINT_PAUSED_FIELD = "companionEndpointPaused";
/** The control plane ended this computer's secure address for old phone apps: phones connect through the relay only. */
export const COMPANION_TUNNEL_RETIRED_FIELD = "companionTunnelRetired";
/** The control plane holds no address for this computer although an old phone would use one (ended for old phone
* apps, or every account full): the watchdog keeps asking, so the address returns when the relay is switched off
* or room frees up. */
export const COMPANION_TUNNEL_WAITING_FIELD = "companionTunnelWaiting";
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const INSTALLATION_ID = UUID;
const INSTALLATION_CREDENTIAL = /^omb_install_[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}$/;
const DEFAULT_HEALTH_CACHE_MS = 30_000;
const DEFAULT_AUTO_RETRY_BASE_MS = 10 * 60_000;
const DEFAULT_AUTO_RETRY_MAX_MS = 6 * 60 * 60_000;
const DEFAULT_ENDPOINT_CHECK_INTERVAL_MS = 15 * 60_000;
const DEFAULT_FIRST_ENDPOINT_CHECK_MS = 2 * 60_000;
// The hour covers installing the phone app after the QR shows; the random spread keeps release waves inside Cloudflare's API budget.
const DEFAULT_RELEASE_GRACE_MS = 60 * 60_000;
const DEFAULT_RELEASE_JITTER_MS = 3 * 60 * 60_000;
const REQUEST_WINDOW_MS = 60 * 60_000;
const AUTO_RETRY_CODES = new Set([
"endpoint_capacity",
"endpoint_rate_limited",
"endpoint_unavailable",
"endpoint_busy",
"network_unavailable",
"control_plane_unavailable",
"internal_error",
"rate_limited",
"credential_rotation_rate_limited",
]);
const ENDPOINT_NEEDS_RECONCILE = new Set(["deleting", "deleted", "error"]);
/** "ready" too: the connector verifies its route once, so after a long sleep it can report ready for a reclaimed tunnel. */
const WATCHDOG_CHECK_STATES = new Set(["starting", "ready", "retrying", "error"]);
const CONNECTOR_FAILING_STATES = new Set(["retrying", "error"]);
const defaultSetTimer = (callback, milliseconds) => {
const timer = setTimeout(callback, milliseconds);
timer.unref?.();
return timer;
};
const ownString = (document, field) =>
typeof document?.[field] === "string" ? document[field] : "";
/** Development must name an exact origin so an OTP or bearer never reaches an accidental host; an invalid override disables the feature. */
export function resolveCompanionControlPlaneURL({
isPackaged,
environment = process.env,
} = {}) {
if (Object.hasOwn(environment, "OMB_CONTROL_PLANE_URL")) {
return normalizeControlPlaneURL(environment.OMB_CONTROL_PLANE_URL);
}
return isPackaged ? DEFAULT_COMPANION_CONTROL_PLANE_URL : "";
}
export function companionAccountCleanupPending(credentials) {
return credentials?.[COMPANION_ACCOUNT_CLEANUP_PENDING_FIELD] === true;
}
/** The control plane ended this signed-in computer's secure address for old phone apps ("retired"), and none came back. */ export const companionTunnelRetired = (credentials) => Boolean(storedAccount(credentials)) && !managedCompanionTunnelAccess(credentials) && credentials?.[COMPANION_TUNNEL_RETIRED_FIELD] === true;
function storedAccount(credentials) {
const email = normalizeAccountEmail(ownString(credentials, COMPANION_ACCOUNT_EMAIL_FIELD));
const userId = ownString(credentials, COMPANION_ACCOUNT_USER_ID_FIELD);
if (!email || !userId || userId.length > 256) return null;
return {
email,
userId,
accountToken: ownString(credentials, COMPANION_ACCOUNT_TOKEN_FIELD),
installationId: INSTALLATION_ID.test(ownString(credentials, COMPANION_INSTALLATION_ID_FIELD))
? credentials[COMPANION_INSTALLATION_ID_FIELD]
: "",
installationCredential: INSTALLATION_CREDENTIAL.test(
ownString(credentials, COMPANION_INSTALLATION_CREDENTIAL_FIELD),
)
? credentials[COMPANION_INSTALLATION_CREDENTIAL_FIELD]
: "",
};
}
function withoutCompanionAccount(credentials) {
const next = withoutManagedCompanionTunnelAccess(credentials);
for (const field of [
COMPANION_ACCOUNT_TOKEN_FIELD,
COMPANION_ACCOUNT_USER_ID_FIELD,
COMPANION_ACCOUNT_EMAIL_FIELD,
COMPANION_INSTALLATION_ID_FIELD,
COMPANION_INSTALLATION_CREDENTIAL_FIELD,
COMPANION_INSTALLATION_EXPIRY_FIELD,
COMPANION_ACCOUNT_CLEANUP_PENDING_FIELD,
COMPANION_ENDPOINT_RELEASE_PENDING_FIELD,
COMPANION_ENDPOINT_PAUSED_FIELD,
COMPANION_TUNNEL_RETIRED_FIELD,
COMPANION_TUNNEL_WAITING_FIELD,
]) {
delete next[field];
}
// The installation UUID survives sign-out so a same-account recovery adopts the existing server record.
return next;
}
function withInstallationCredentials(credentials, installation) {
const next = {
...credentials,
[COMPANION_INSTALLATION_ID_FIELD]: installation.installation.id,
[COMPANION_INSTALLATION_CREDENTIAL_FIELD]: installation.credential,
};
if (Number.isSafeInteger(installation.credentialExpiresAt)) {
next[COMPANION_INSTALLATION_EXPIRY_FIELD] = installation.credentialExpiresAt;
} else {
delete next[COMPANION_INSTALLATION_EXPIRY_FIELD];
}
return next;
}
function withProvisionedAccount(credentials, { accountToken, user, installation, provision }) {
const next = {
...withInstallationCredentials(withManagedCompanionTunnelAccess(credentials, provision), installation),
[COMPANION_ACCOUNT_TOKEN_FIELD]: accountToken,
[COMPANION_ACCOUNT_USER_ID_FIELD]: user.id,
[COMPANION_ACCOUNT_EMAIL_FIELD]: user.email,
};
delete next[COMPANION_ACCOUNT_CLEANUP_PENDING_FIELD];
delete next[COMPANION_ENDPOINT_RELEASE_PENDING_FIELD];
delete next[COMPANION_ENDPOINT_PAUSED_FIELD];
delete next[COMPANION_TUNNEL_RETIRED_FIELD];
delete next[COMPANION_TUNNEL_WAITING_FIELD];
return next;
}
function withAuthenticatedAccount(
credentials,
{ accountToken, user, clientInstanceId, preserveCleanupPending = false },
) {
const next = {
...credentials,
[COMPANION_ACCOUNT_TOKEN_FIELD]: accountToken,
[COMPANION_ACCOUNT_USER_ID_FIELD]: user.id,
[COMPANION_ACCOUNT_EMAIL_FIELD]: user.email,
};
if (!UUID.test(ownString(next, COMPANION_CLIENT_INSTANCE_FIELD))) {
next[COMPANION_CLIENT_INSTANCE_FIELD] = clientInstanceId;
}
if (preserveCleanupPending) {
next[COMPANION_ACCOUNT_CLEANUP_PENDING_FIELD] = true;
} else {
delete next[COMPANION_ACCOUNT_CLEANUP_PENDING_FIELD];
}
return next;
}
function publicState({ available, status, email, endpoint, message, paused, tunnelRetired }) {
const state = { available: Boolean(available), status };
if (paused === true) state.paused = true;
if (tunnelRetired === true) state.tunnelRetired = true;
const normalizedEmail = normalizeAccountEmail(email);
if (normalizedEmail) state.email = normalizedEmail;
const accessEndpoint = (() => {
if (typeof endpoint !== "string") return "";
try {
const parsed = new URL(endpoint);
return parsed.protocol === "https:" && parsed.pathname === "/" && !parsed.search && !parsed.hash
? parsed.origin
: "";
} catch {
return "";
}
})();
if (accessEndpoint) state.endpoint = accessEndpoint;
const safeMessage = typeof message === "string" && message.length >= 1 && message.length <= 280
? message
: null;
if (safeMessage) state.message = safeMessage;
return Object.freeze(state);
}
/** Nothing this service returns may contain an account bearer, installation credential, connector token or Cloudflare resource id. */
export function createCompanionAccountService({
client,
readCredentials,
updateCredentials,
identity,
newClientInstanceId,
activatePersistedEndpoint = async () => ({ status: "stopped", ready: false }),
stopManagedEndpoint = async () => {},
managedConnectionState = () => ({ status: "stopped", ready: false }),
companionIsOn = () => false,
now = Date.now,
healthCacheMs = DEFAULT_HEALTH_CACHE_MS,
autoRecover = false,
setTimer = defaultSetTimer,
clearTimer = clearTimeout,
random = Math.random,
autoRetryBaseMs = DEFAULT_AUTO_RETRY_BASE_MS,
autoRetryMaxMs = DEFAULT_AUTO_RETRY_MAX_MS,
endpointCheckIntervalMs = DEFAULT_ENDPOINT_CHECK_INTERVAL_MS,
firstEndpointCheckMs = DEFAULT_FIRST_ENDPOINT_CHECK_MS,
onDemand = false,
managedEndpointNeeded = async () => null,
/** MausBot Relay: `capable` makes a "no" need hold even right after sign-in; `legacyDevices` counts old phones. */
relay = { capable: () => false, legacyDevices: async () => null },
releaseGraceMs = DEFAULT_RELEASE_GRACE_MS,
releaseJitterMs = DEFAULT_RELEASE_JITTER_MS,
// Only the code, HTTP status, request id and retry delay: never an email, token, credential or address.
log = () => {},
} = {}) {
const configured = Boolean(client);
let healthy = false;
let lastHealthCheck = null;
let healthProbe = null;
let phase = null;
let transition = Promise.resolve();
let autoRetryTimer = null;
let autoRetryAttempt = 0;
let watchdogTimer = null;
let lastEndpointCheck = null;
let requestedAt = null;
let disposed = false;
const serialize = (work) => {
const next = transition.then(work, work);
transition = next.then(
() => {},
() => {},
);
return next;
};
const credentials = () => readCredentials?.() ?? {};
const cancelAutoRetry = () => {
if (autoRetryTimer !== null) clearTimer(autoRetryTimer);
autoRetryTimer = null;
};
const resetAutoRetry = () => {
cancelAutoRetry();
autoRetryAttempt = 0;
};
let retryFromTimer = () => Promise.resolve();
const scheduleAutoRetry = (error) => {
if (!autoRecover || disposed || !configured) return;
if (!(error instanceof ControlPlaneError) || !AUTO_RETRY_CODES.has(error.code)) return;
const document = credentials();
if (!storedAccount(document) || companionAccountCleanupPending(document)) return;
cancelAutoRetry();
const backoff = Math.min(
autoRetryMaxMs,
autoRetryBaseMs * 2 ** Math.min(autoRetryAttempt, 20),
);
const jittered = Math.round(backoff * (0.8 + 0.4 * random()));
const delay = Math.min(autoRetryMaxMs, Math.max(jittered, error.retryAfterMs ?? 0));
autoRetryAttempt += 1;
writeLog(`companion account: retrying ${error.code} in ${Math.round(delay / 1_000)}s (attempt ${autoRetryAttempt})`);
autoRetryTimer = setTimer(() => {
autoRetryTimer = null;
void retryFromTimer().catch(() => {});
}, delay);
};
const probeControlPlane = async ({ force = false } = {}) => {
if (!configured) return false;
const checkedAt = now();
if (
!force &&
lastHealthCheck !== null &&
checkedAt - lastHealthCheck < Math.max(0, healthCacheMs)
) {
return healthy;
}
if (healthProbe) return healthProbe;
healthProbe = (async () => {
try {
await client.health();
healthy = true;
} catch {
healthy = false;
}
lastHealthCheck = now();
return healthy;
})().finally(() => {
healthProbe = null;
});
return healthProbe;
};
const requireHealthyControlPlane = async () => {
if (!(await probeControlPlane({ force: true }))) {
throw new ControlPlaneError("control_plane_unavailable");
}
};
const settledState = () => {
if (!configured) {
return publicState({
available: false,
status: "signed-out",
message: FRIENDLY_MESSAGES.control_plane_unavailable,
});
}
const document = credentials();
const account = storedAccount(document);
const persistedAccess = managedCompanionTunnelAccess(document);
const available = healthy || Boolean(account);
const paused = Boolean(account) && !persistedAccess && document[COMPANION_ENDPOINT_PAUSED_FIELD] === true, tunnelRetired = companionTunnelRetired(document);
if (!healthy) {
return publicState({
available,
status: account ? "error" : "signed-out",
email: account?.email,
endpoint: persistedAccess?.endpoint,
message: FRIENDLY_MESSAGES.control_plane_unavailable,
paused,
tunnelRetired,
});
}
if (
phase &&
["connecting", "error"].includes(phase.status) &&
account &&
persistedAccess &&
!companionAccountCleanupPending(document) &&
managedConnectionState?.()?.ready === true
) {
phase = null;
}
if (phase) return publicState({ available, ...phase, paused, tunnelRetired });
if (!account) return publicState({ available, status: "signed-out" });
const access = persistedAccess;
if (!access) {
if (onDemand) return publicState({ available, status: "ready", email: account.email, paused, tunnelRetired });
return publicState({
available,
status: "error",
email: account.email,
message: "This computer still needs a secure address. Try again; local pairing continues to work.",
});
}
const connection = managedConnectionState?.() ?? {};
if (companionIsOn()) {
if (["starting", "retrying"].includes(connection.status)) {
return publicState({
available,
status: "connecting",
email: account.email,
endpoint: access.endpoint,
message: "The secure connection is starting. Local pairing remains available.",
});
}
if (["unavailable", "error"].includes(connection.status)) {
return publicState({
available,
status: "error",
email: account.email,
endpoint: access.endpoint,
message: "The secure connection needs attention. Local pairing still works.",
});
}
}
return publicState({
available,
status: "ready",
email: account.email,
endpoint: access.endpoint,
});
};
const ensureClientIdentity = async () => {
const existing = ownString(credentials(), COMPANION_CLIENT_INSTANCE_FIELD);
if (UUID.test(existing)) return existing;
const candidate = newClientInstanceId?.();
if (!UUID.test(candidate ?? "")) throw new Error("A stable computer identity could not be created");
await updateCredentials((document) => {
if (UUID.test(ownString(document, COMPANION_CLIENT_INSTANCE_FIELD))) return document;
return { ...document, [COMPANION_CLIENT_INSTANCE_FIELD]: candidate };
});
return ownString(credentials(), COMPANION_CLIENT_INSTANCE_FIELD);
};
const markCleanupPending = async () => {
await updateCredentials((document) => ({
...document,
[COMPANION_ACCOUNT_CLEANUP_PENDING_FIELD]: true,
}));
};
const clearAfterCleanup = async () => {
await updateCredentials(withoutCompanionAccount);
await stopManagedEndpoint();
phase = null;
};
/** True only once revocation has scheduled endpoint cleanup, so the local retry credentials may be forgotten. */
const cleanupCurrentAccount = async ({ markPending = true } = {}) => {
const document = credentials();
const account = storedAccount(document);
if (!account) {
await clearAfterCleanup();
return true;
}
if (markPending && !companionAccountCleanupPending(document)) await markCleanupPending();
await stopManagedEndpoint();
// Delete first for immediate feedback; revocation below stays mandatory.
if (account.installationCredential) {
try {
await client.deleteEndpoint(account.installationCredential);
} catch {
}
}
if (!account.accountToken) {
throw new ControlPlaneError("signed_out", 401);
}
// A lost response can leave a server installation with no local ID, so revoke every record matching this computer's UUID before forgetting the bearer.
const clientInstanceId = ownString(document, COMPANION_CLIENT_INSTANCE_FIELD);
if (!UUID.test(clientInstanceId)) {
throw new ControlPlaneError("invalid_client_identity");
}
if (account.installationId) {
await client.revokeInstallation(account.accountToken, account.installationId).catch(() => {});
}
const installations = await client.listInstallations(account.accountToken);
for (const installation of installations) {
if (
installation.clientInstanceId !== clientInstanceId &&
installation.id !== account.installationId
) {
continue;
}
try {
await client.revokeInstallation(account.accountToken, installation.id);
} catch (error) {
if (!(error instanceof ControlPlaneError) || error.status !== 404) throw error;
}
}
try {
await client.signOut(account.accountToken);
} catch {
}
await clearAfterCleanup();
return true;
};
/** Finish a give-back the control plane left pending (on demand): until
* that removal is done it creates no address for this installation. Once
* its own sweep has done it, the DELETE is answered at once. If this one
* fails too, the request that follows says why. */
const finishPendingRelease = async () => {
const document = credentials();
const credential = storedAccount(document)?.installationCredential;
if (document[COMPANION_ENDPOINT_RELEASE_PENDING_FIELD] !== true || !credential) return;
try {
await client.deleteEndpoint(credential);
} catch {
return;
}
await updateCredentials((current) => {
const next = { ...current };
delete next[COMPANION_ENDPOINT_RELEASE_PENDING_FIELD];
return next;
});
};
const provision = async ({ accountToken, user }) => {
const clientInstanceId = await ensureClientIdentity();
await finishPendingRelease();
const before = credentials();
const previous = storedAccount(before);
const installation = await client.ensureInstallation({
accountToken,
currentCredential:
previous?.userId === user.id ? previous.installationCredential : "",
clientInstanceId,
name: identity.name,
platform: identity.platform,
appVersion: identity.appVersion, ...(identity.kind ? { kind: identity.kind } : {}),
});
// Persist the verified installation before provisioning, or each Retry rotates the credential until rate-limited.
try {
await updateCredentials((document) => withInstallationCredentials(document, installation));
} catch (error) {
if (previous?.installationId !== installation.installation.id ||
previous?.installationCredential !== installation.credential) {
await client.revokeInstallation(accountToken, installation.installation.id).catch(() => {});
}
throw error;
}
const withoutTunnel = async () => {
await updateCredentials((document) => withAuthenticatedAccount(document, { accountToken, user, clientInstanceId }));
resetAutoRetry();
return (phase = null), settledState();
};
if (onDemand && (await currentNeed()) === false) return withoutTunnel();
let relayOnly = null;
const endpoint = await client.ensureEndpoint(installation.credential, { appVersion: identity.appVersion, ...(identity.kind ? { kind: identity.kind } : {}), ...(await relay.legacyDevices().then((count) => (count === null ? {} : { legacyDevices: count }))) })
.catch((error) => {
if (error?.code !== "relay_capable") throw error;
relayOnly = error.tunnel ?? "none";
return null;
});
if (!endpoint) {
// The control plane holds no tunnel for this computer any more: stop the connector and forget the address, so
// nothing keeps dialling or advertising one that is gone. "retired" is remembered for the notice in Settings.
if (managedCompanionTunnelAccess(credentials())) {
await stopManagedEndpoint();
await updateCredentials(withoutManagedCompanionTunnelAccess);
writeLog(`companion account: the secure address ended (${relayOnly}); phones connect through the relay`);
}
await updateCredentials((document) => {
const next = { ...document };
if (relayOnly === "retired") next[COMPANION_TUNNEL_RETIRED_FIELD] = true;
else delete next[COMPANION_TUNNEL_RETIRED_FIELD];
// With the address forgotten nothing else would ask again: this keeps the watchdog asking.
if (relayOnly === "retired" || relayOnly === "full") next[COMPANION_TUNNEL_WAITING_FIELD] = true;
else delete next[COMPANION_TUNNEL_WAITING_FIELD];
return next;
});
return withoutTunnel();
}
try {
await updateCredentials((document) =>
withProvisionedAccount(document, {
accountToken,
user,
installation,
provision: endpoint,
}),
);
} catch (error) {
// Best-effort cleanup avoids an invisible tunnel; no secret ever reaches a log or exception.
await client.deleteEndpoint(installation.credential).catch(() => {});
await client.revokeInstallation(accountToken, installation.installation.id).catch(() => {});
throw error;
}
resetAutoRetry();
const connection = await activatePersistedEndpoint();
phase = null;
if (
companionIsOn() &&
connection &&
["unavailable", "error"].includes(connection.status)
) {
phase = {
status: "error",
email: user.email,
endpoint: endpoint.endpoint.url,
message: "The address is ready, but this app could not start its secure connection. Local pairing still works.",
};
}
return settledState();
};
const writeLog = (line) => {
try {
log(line);
} catch {
}
};
const failAction = (
error,
{ email, expiredSessionIsSignedOut = false, signedOut = false } = {},
) => {
const message = friendlyCompanionAccountError(error);
writeLog(
error instanceof ControlPlaneError
? `companion account: setup failed code=${error.code} status=${error.status || "none"} ref=${error.requestId || "none"}`
: "companion account: setup failed on this computer (not a control-plane answer)",
);
phase = {
status:
signedOut ||
(expiredSessionIsSignedOut && error instanceof ControlPlaneError && error.status === 401)
? "signed-out"
: "error",
email,
message,
};
return message;
};
const requestCode = (rawEmail) => serialize(async () => {
if (!configured) throw new Error(FRIENDLY_MESSAGES.control_plane_unavailable);
const email = normalizeAccountEmail(rawEmail);
let requested;
try {
await requireHealthyControlPlane();
requested = await client.requestOTP(email);
} catch (error) {
const message = failAction(error, { email, signedOut: true });
throw new Error(message);
}
phase = {
status: "signed-out",
email: requested.email,
};
return settledState();
});
const verifyCode = (rawEmail, rawCode) => serialize(async () => {
if (!configured) throw new Error(FRIENDLY_MESSAGES.control_plane_unavailable);
requestedAt = now();
const email = normalizeAccountEmail(rawEmail);
phase = { status: "connecting", email };
let verified;
try {
await requireHealthyControlPlane();
verified = await client.verifyOTP(email, rawCode);
} catch (error) {
const message = failAction(error, { email, signedOut: true });
throw new Error(message);
}
const previous = storedAccount(credentials());
const refreshingPendingCleanup = Boolean(
previous &&
previous.userId === verified.user.id &&
companionAccountCleanupPending(credentials()),
);
let authenticatedPersisted = false;
try {
if (previous && previous.userId !== verified.user.id) {
await cleanupCurrentAccount();
}
const existingIdentity = ownString(credentials(), COMPANION_CLIENT_INSTANCE_FIELD);
const clientInstanceId = UUID.test(existingIdentity)
? existingIdentity
: newClientInstanceId?.();
if (!UUID.test(clientInstanceId ?? "")) {
throw new Error("A stable computer identity could not be created");
}
await updateCredentials((document) =>
withAuthenticatedAccount(document, {
...verified,
clientInstanceId,
preserveCleanupPending: refreshingPendingCleanup,
}),
);
authenticatedPersisted = true;
if (refreshingPendingCleanup) {
// Finish the interrupted sign-out; never turn it into a new endpoint provisioning.
await cleanupCurrentAccount({ markPending: false });
return settledState();
}
return await provision(verified);
} catch (error) {
if (!authenticatedPersisted) await client.signOut(verified.accountToken).catch(() => {});
failAction(error, {
email: authenticatedPersisted ? verified.user.email : previous?.email ?? email,
expiredSessionIsSignedOut: authenticatedPersisted,
});
if (authenticatedPersisted) scheduleAutoRetry(error);
return settledState();
}
});
const retryWork = async () => {
if (!configured) return settledState();
const account = storedAccount(credentials());
if (!account) {
phase = null;
return settledState();
}
if (companionAccountCleanupPending(credentials())) {
phase = { status: "connecting", email: account.email };
try {
await requireHealthyControlPlane();
await cleanupCurrentAccount({ markPending: false });
} catch (error) {
failAction(error, { email: account.email, expiredSessionIsSignedOut: true });
}
return settledState();
}
phase = { status: "connecting", email: account.email };
try {
await requireHealthyControlPlane();
return await provision({
accountToken: account.accountToken,
user: { id: account.userId, email: account.email },
});
} catch (error) {
failAction(error, { email: account.email, expiredSessionIsSignedOut: true });
scheduleAutoRetry(error);
return settledState();
}
};
const retry = () => serialize(async () => {
requestedAt = now();
return retryWork();
});
/** Re-provisions only on a definitive "no endpoint" answer; a network or service failure keeps everything as is. */
const reconcileIfEndpointGone = async ({ onGone = retryWork } = {}) => {
const document = credentials();
const account = storedAccount(document);
const access = managedCompanionTunnelAccess(document);
if (
!account ||
!access ||
!account.installationCredential ||
companionAccountCleanupPending(document) ||
typeof client.getEndpoint !== "function"
) {
return null;
}
lastEndpointCheck = now();
let endpoint;
try {
endpoint = await client.getEndpoint(account.installationCredential);
} catch (error) {
if (error instanceof ControlPlaneError && error.status === 401) return onGone();
return null;
}
if (endpoint && endpoint.url === access.endpoint && !ENDPOINT_NEEDS_RECONCILE.has(endpoint.status)) {
return null;
}
return onGone();
};
const endpointCheckDue = () =>
lastEndpointCheck === null || now() - lastEndpointCheck >= endpointCheckIntervalMs / 2;
/** Only while Remote access is on: a check-in marks an unused tunnel as recently seen, which blocks its reclaim. */
const recoveryDue = ({ atLaunch = false } = {}) => {
if (!autoRecover || !companionIsOn()) return false;
if (atLaunch) return true;
if (autoRetryTimer !== null) return false;
const connection = managedConnectionState?.() ?? {};
return WATCHDOG_CHECK_STATES.has(connection.status) && endpointCheckDue();
};
const requestedRecently = () => requestedAt !== null && now() - requestedAt < REQUEST_WINDOW_MS;
const savedAddress = (document) => normalizeManagedCompanionEndpoint(document[MANAGED_COMPANION_ENDPOINT_FIELD]);
let phonesUnused = false;
/** "unused" (paired phones, none seen for 30 days) reads as false and is remembered for the release. */
const currentNeed = async () => {
const answer = await Promise.resolve().then(() => managedEndpointNeeded()).catch(() => null);
phonesUnused = answer === MANAGED_ENDPOINT_PHONES_UNUSED;
const need = phonesUnused ? false : typeof answer === "boolean" ? answer : null;
if (need === false && relay.capable()) return false;
if (requestedRecently()) return (phonesUnused = false), true;
return need;
};
const releaseScheduledAt = () => {
const at = credentials()[MANAGED_COMPANION_RELEASE_AT_FIELD];
return Number.isSafeInteger(at) && at > 0 ? at : null;
};
const cancelRelease = async () => {
if (releaseScheduledAt() === null) return;
await updateCredentials((document) => {
const next = { ...document };
delete next[MANAGED_COMPANION_RELEASE_AT_FIELD];
return next;
});
};
/** The connector stops and the address is withdrawn before the DELETE, so nothing can pair to an address being removed. */
const releaseAddress = async () => {
const need = await currentNeed();
const pause = phonesUnused;
if (need !== false) {
if (need) await cancelRelease();
return settledState();
}
const document = credentials();
const account = storedAccount(document);
if (
!account?.installationCredential ||
!managedCompanionTunnelAccess(document) ||
companionAccountCleanupPending(document)
) {
return settledState();
}
await stopManagedEndpoint();
let removalPending = false;
try {
await client.deleteEndpoint(account.installationCredential);
} catch (error) {
removalPending = error instanceof ControlPlaneError && error.code === "endpoint_cleanup_pending";
const handedOver = removalPending || (error instanceof ControlPlaneError && error.status === 401);
if (!handedOver) {
writeLog(
error instanceof ControlPlaneError
? `companion account: could not give the secure address back code=${error.code} status=${error.status || "none"} ref=${error.requestId || "none"}`
: "companion account: could not give the secure address back (not a control-plane answer)",
);
await activatePersistedEndpoint();
return settledState();
}
}
await updateCredentials((current) => ({
...withoutManagedCompanionTunnelAccess(current),
...(removalPending ? { [COMPANION_ENDPOINT_RELEASE_PENDING_FIELD]: true } : {}),
...(pause ? { [COMPANION_ENDPOINT_PAUSED_FIELD]: true } : {}),
}));
resetAutoRetry();
phase = null;
writeLog(pause
? "companion account: gave the secure address back; no paired phone was used in 30 days"
: "companion account: gave the secure address back; no device needs it");
return settledState();
};
const reviewAddress = async ({ atLaunch = false } = {}) => {
const document = credentials();
if (!storedAccount(document) || companionAccountCleanupPending(document)) return null;
if (!managedCompanionTunnelAccess(document)) {
// Waiting: the control plane ended the address or had no room. Asked again on every watchdog tick while an
// old phone still needs it, as a desktop that kept its address does.
const waiting = document[COMPANION_TUNNEL_WAITING_FIELD] === true;
if ((!savedAddress(document) && !waiting) || autoRetryTimer !== null) return null;
return (await currentNeed()) === true ? retryWork() : null;
}
const need = await currentNeed();
if (need !== false) {
if (need) await cancelRelease();
if (need === null && atLaunch) return null;
return recoveryDue({ atLaunch }) ? reconcileIfEndpointGone() : null;
}
const releaseAt = releaseScheduledAt();
if (releaseAt !== null && now() >= releaseAt) return releaseAddress();
const connection = managedConnectionState?.() ?? {};
if (
companionIsOn() &&
CONNECTOR_FAILING_STATES.has(connection.status) &&
(atLaunch || endpointCheckDue())
) {
const released = await reconcileIfEndpointGone({ onGone: releaseAddress });
if (released) return released;
}
if (releaseAt === null || releaseAt - now() > releaseGraceMs + releaseJitterMs) {
const delay = releaseGraceMs + Math.round(random() * releaseJitterMs);
await updateCredentials((current) => managedCompanionTunnelAccess(current)
? { ...current, [MANAGED_COMPANION_RELEASE_AT_FIELD]: now() + delay }
: current);
writeLog(`companion account: no device needs the secure address; giving it back in ${Math.round(delay / 60_000)} min`);
}
return null;
};
const retryIfWanted = async () => {
if (requestedRecently()) return retryWork();
if (savedAddress(credentials()) && (await currentNeed()) !== false) {
return retryWork();
}
resetAutoRetry();
if (phase?.status === "error") phase = null;
writeLog("companion account: stopped retrying; no device needs a secure address");
return settledState();
};
retryFromTimer = () => serialize(onDemand ? retryIfWanted : retryWork);
const prepare = () => serialize(async () => {
const document = credentials();
if (
!configured ||
!companionIsOn() ||
!storedAccount(document) ||
companionAccountCleanupPending(document)
) {
return settledState();
}
requestedAt = now();
if (!managedCompanionTunnelAccess(document)) return retryWork();
await cancelRelease();
const connection = managedConnectionState?.() ?? {};
if (["stopped", "unconfigured"].includes(connection.status)) {
await activatePersistedEndpoint();
} else if (CONNECTOR_FAILING_STATES.has(connection.status)) {
const repaired = await reconcileIfEndpointGone();
if (repaired) return repaired;
}
return settledState();
});
const watchdogTick = () => {
watchdogTimer = null;
if (disposed) return;
watchdogTimer = setTimer(watchdogTick, endpointCheckIntervalMs);
if (phase?.status === "signed-out") return;
if (onDemand) {
void serialize(() => reviewAddress()).catch(() => {});
return;
}
if (recoveryDue()) void serialize(() => reconcileIfEndpointGone()).catch(() => {});
};
const startWatchdog = () => {
if (!(autoRecover || onDemand) || disposed || !configured || watchdogTimer !== null) return;
watchdogTimer = setTimer(watchdogTick, firstEndpointCheckMs);
};
const signOut = () => serialize(async () => {
resetAutoRetry();
if (!storedAccount(credentials())) {
await clearAfterCleanup();
return settledState();
}
const email = storedAccount(credentials())?.email;
phase = { status: "connecting", email };
try {
// No health preflight: local access must stop on Sign out even when the control plane is down.
await cleanupCurrentAccount();
healthy = true;
lastHealthCheck = now();
return settledState();
} catch (error) {
failAction(error, { email });
return settledState();
}
});
const restore = () => serialize(async () => {
if (!configured) return settledState();
startWatchdog();
if (!(await probeControlPlane({ force: true }))) return settledState();
if (companionAccountCleanupPending(credentials())) return retryWork();
await ensureClientIdentity();
if (onDemand) {
const reviewed = await reviewAddress({ atLaunch: true });
if (reviewed) return reviewed;
phase = null;
return settledState();
}
const account = storedAccount(credentials());
if (account && !managedCompanionTunnelAccess(credentials())) return retryWork();
if (autoRecover && account && companionIsOn()) {
const reconciled = await reconcileIfEndpointGone();
if (reconciled) return reconciled;
}
phase = null;
return settledState();
});
const dispose = () => {
disposed = true;
cancelAutoRetry();
if (watchdogTimer !== null) clearTimer(watchdogTimer);
watchdogTimer = null;
};
return Object.freeze({
state: async () => {
await probeControlPlane();
return settledState();
},
requestCode,
verifyCode,
retry,
prepare,
signOut,
restore,
dispose,
});
}