fix(mobile): keep tokenless connections alive across launch and resume
A server running with authentication disabled pairs and connects fine, but the saved connection has no bearer token. Auto-connect silently bailed on the missing token and the resume reprobe reported it as 'unreachable', so every return to the app kicked the user to the connect screen. Treat a saved tokenless connection as valid: probe it without a bearer and let the probe decide — auth disabled connects, auth enabled later reports needs-login. Bail out only when an expected token cannot be read.
This commit is contained in:
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- **Chat:** new messages now remain at the end of the conversation instead of jumping before older messages after the message ID sequence rolls over; history loading, revert, and redo follow the same chronological order.
|
- **Chat:** new messages now remain at the end of the conversation instead of jumping before older messages after the message ID sequence rolls over; history loading, revert, and redo follow the same chronological order.
|
||||||
- **Stability:** a single internal error no longer shuts down the local server, which made the instance unreachable until it was restarted; the error is logged and the server keeps running.
|
- **Stability:** a single internal error no longer shuts down the local server, which made the instance unreachable until it was restarted; the error is logged and the server keeps running.
|
||||||
|
- Mobile: connecting to a server that has authentication disabled now survives closing and reopening the app — auto-reconnect and the return-to-app check no longer treat the missing password token as a lost connection and kick back to the connect screen.
|
||||||
- Browser: restoring or opening a dev server preview while connected to an instance over a relay or other non-standard address no longer crashes the app; the preview reports the tunnel as unavailable instead.
|
- Browser: restoring or opening a dev server preview while connected to an instance over a relay or other non-standard address no longer crashes the app; the preview reports the tunnel as unavailable instead.
|
||||||
|
|
||||||
## [1.18.3] - 2026-08-14
|
## [1.18.3] - 2026-08-14
|
||||||
|
|||||||
@@ -1015,20 +1015,20 @@ export const autoConnectLastInstance = async (options?: { fast?: boolean; skipIf
|
|||||||
logConnect('auto-connect:start', { hasCandidate: Boolean(candidate), fast });
|
logConnect('auto-connect:start', { hasCandidate: Boolean(candidate), fast });
|
||||||
if (!candidate) return { status: 'no-candidate' };
|
if (!candidate) return { status: 'no-candidate' };
|
||||||
|
|
||||||
// The runtime transport needs a bearer token; only auto-connect when one is
|
// The runtime transport authenticates with a bearer token when the server
|
||||||
// already saved. A missing/expired token must go through the login UI.
|
// issued one. A connection saved WITHOUT a token means its last successful
|
||||||
|
// connect was tokenless (server auth disabled) — probe it the same way; the
|
||||||
|
// probe itself reports needs-login if the server has since enabled auth. Only
|
||||||
|
// an EXPECTED token that cannot be read must go through the login UI.
|
||||||
let token: string | undefined;
|
let token: string | undefined;
|
||||||
if (isCapacitorApp()) {
|
if (isCapacitorApp()) {
|
||||||
if (!candidate.hasToken) {
|
if (candidate.hasToken) {
|
||||||
return { status: 'no-candidate' };
|
token = await readSecureToken(secureTokenKeyOf(candidate));
|
||||||
}
|
if (!token) return { status: 'no-candidate' };
|
||||||
token = await readSecureToken(secureTokenKeyOf(candidate));
|
|
||||||
if (!token) {
|
|
||||||
return { status: 'no-candidate' };
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
token = candidate.clientToken;
|
token = candidate.clientToken;
|
||||||
if (!token) return { status: 'no-candidate' };
|
if (!token && candidate.hasToken) return { status: 'no-candidate' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast probe by default: the cold-launch splash should decide in a couple of
|
// Fast probe by default: the cold-launch splash should decide in a couple of
|
||||||
@@ -1050,7 +1050,7 @@ export const autoConnectLastInstance = async (options?: { fast?: boolean; skipIf
|
|||||||
return { status: 'no-candidate' };
|
return { status: 'no-candidate' };
|
||||||
}
|
}
|
||||||
await upsertMobileConnection({ id: candidate.id, label: candidate.label, candidates: candidate.candidates }); // bump lastUsedAt (keeps token)
|
await upsertMobileConnection({ id: candidate.id, label: candidate.label, candidates: candidate.candidates }); // bump lastUsedAt (keeps token)
|
||||||
switchToTransport(result.transport, token, { runtimeKey: secureTokenKeyOf(candidate) });
|
switchToTransport(result.transport, token ?? null, { runtimeKey: secureTokenKeyOf(candidate) });
|
||||||
return { status: 'connected' };
|
return { status: 'connected' };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1209,11 +1209,15 @@ export const reprobeActiveConnection = async (options?: { fast?: boolean }): Pro
|
|||||||
} else {
|
} else {
|
||||||
token = active.clientToken;
|
token = active.clientToken;
|
||||||
}
|
}
|
||||||
if (!token) {
|
// Tokenless is valid (server auth disabled — the probe reports needs-login if
|
||||||
logConnect('reprobe:no-token', { hasToken: Boolean(active.hasToken) });
|
// that changed); bail only when an EXPECTED token cannot be read. 'unreachable'
|
||||||
|
// (not needs-login) so the resume retry ladder re-reads the token — a transient
|
||||||
|
// secure-storage failure must not force a re-login.
|
||||||
|
if (!token && active.hasToken) {
|
||||||
|
logConnect('reprobe:no-token', { hasToken: true });
|
||||||
return 'unreachable';
|
return 'unreachable';
|
||||||
}
|
}
|
||||||
logConnect('reprobe:start', { candidates: active.candidates.map((c) => c.kind), fast });
|
logConnect('reprobe:start', { candidates: active.candidates.map((c) => c.kind), fast, hasToken: Boolean(token) });
|
||||||
|
|
||||||
const currentIndex = active.candidates.findIndex(
|
const currentIndex = active.candidates.findIndex(
|
||||||
(candidate) => transportMatchesCurrentRuntime(candidate.kind === 'relay' ? { kind: 'relay', relay: candidate.relay } : { kind: 'direct', url: candidate.url }),
|
(candidate) => transportMatchesCurrentRuntime(candidate.kind === 'relay' ? { kind: 'relay', relay: candidate.relay } : { kind: 'direct', url: candidate.url }),
|
||||||
@@ -1225,7 +1229,7 @@ export const reprobeActiveConnection = async (options?: { fast?: boolean }): Pro
|
|||||||
logConnect('reprobe:better', { status: better.status, probed: higher.length });
|
logConnect('reprobe:better', { status: better.status, probed: higher.length });
|
||||||
if (better.status === 'ok') {
|
if (better.status === 'ok') {
|
||||||
await upsertMobileConnection({ id: active.id, label: active.label, candidates: active.candidates });
|
await upsertMobileConnection({ id: active.id, label: active.label, candidates: active.candidates });
|
||||||
switchToTransport(better.transport, token, { runtimeKey: secureTokenKeyOf(active) });
|
switchToTransport(better.transport, token ?? null, { runtimeKey: secureTokenKeyOf(active) });
|
||||||
return 'switched';
|
return 'switched';
|
||||||
}
|
}
|
||||||
// The shared token was explicitly rejected — no transport will accept it.
|
// The shared token was explicitly rejected — no transport will accept it.
|
||||||
@@ -1251,7 +1255,7 @@ export const reprobeActiveConnection = async (options?: { fast?: boolean }): Pro
|
|||||||
logConnect('reprobe:fallback', { status: fallback.status, probed: lower.length });
|
logConnect('reprobe:fallback', { status: fallback.status, probed: lower.length });
|
||||||
if (fallback.status === 'ok') {
|
if (fallback.status === 'ok') {
|
||||||
await upsertMobileConnection({ id: active.id, label: active.label, candidates: active.candidates });
|
await upsertMobileConnection({ id: active.id, label: active.label, candidates: active.candidates });
|
||||||
switchToTransport(fallback.transport, token, { runtimeKey: secureTokenKeyOf(active) });
|
switchToTransport(fallback.transport, token ?? null, { runtimeKey: secureTokenKeyOf(active) });
|
||||||
return 'switched';
|
return 'switched';
|
||||||
}
|
}
|
||||||
if (fallback.status === 'needs-login') return 'needs-login';
|
if (fallback.status === 'needs-login') return 'needs-login';
|
||||||
|
|||||||
Reference in New Issue
Block a user