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, }); }