summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js68
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js15
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js15
3 files changed, 86 insertions, 12 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index 3e3b483..422d90b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -26,6 +26,17 @@ const GroupSettingsPanel = lazy(() => import('./group-settings.js'), 'GroupSetti
import { reportIndexPush } from './index-dock.js';
import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js';
+// While a linking code is shown, how long after a refused try the group is
+// tried again: a person typing the code on another device takes longer, and
+// each try is a connection.
+const DEVICE_RETRY_MS = 5000;
+
+/** The node's `expires_at` (ISO) in epoch seconds; 0 when it gave none. */
+function _untilSeconds(iso) {
+ const ms = Date.parse(iso || '');
+ return Number.isFinite(ms) ? ms / 1000 : 0;
+}
+
/**
* The group shell: everything a group's "applications" (Chat, Files, and
* whatever registers in apps.js next) share — the WebRTC connection, the file
@@ -218,6 +229,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
}, []);
const browserAccessOff = !nativeKeys && userPrefs && userPrefs.browser_access === 'off';
const [deviceCode, setDeviceCode] = useState('');
+ // While a code is shown: until when the node keeps its request (epoch
+ // seconds). The group is tried again after each refusal until then.
+ const deviceWaitRef = useRef(0);
const [codeInput, setCodeInput] = useState('');
// This browser has never derived the passphrase-bundle key (fresh browser,
// cleared storage, or a device-key sign-in). Ask for the passphrase here
@@ -236,9 +250,15 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// set to `true` while looking at one group would otherwise silently disable
// the retry for every group opened afterward in the same session, forever.
const refreshedRef = useRef(false);
+ // Set by "Get a linking code" for the one connection that files the request.
+ const fileDeviceRef = useRef(false);
useEffect(() => {
refreshedRef.current = false;
+ fileDeviceRef.current = false;
setNeedsPass(false);
+ deviceWaitRef.current = 0;
+ setNeedsDevice(false);
+ setDeviceCode('');
}, [groupId]);
const submitJoinCode = useCallback((e) => {
@@ -367,6 +387,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// Dropped by the teardown below, so a transport handed on to a running
// download (`releaseWhenIdle`) stops driving a page that is gone.
let offReconnect = null;
+ let deviceRetry = null;
// The cache is written here and read only by the search page. It used to
// seed this list too, which put a stale index on screen and then raced the
@@ -453,6 +474,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
transport.onDeviceIdentity = (ok) => {
if (!cancelled) setDeviceReady(ok);
};
+ // "Get a linking code" retries with this set: the request is filed
+ // on the connection the node refused, which is closed right after.
+ transport.fileDeviceRequest = fileDeviceRef.current;
try {
ack = await transport.connect(
n.node_id, live, groupId, null, identity, session.bundleKey,
@@ -615,8 +639,12 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
})();
});
- // We are in: an invitation to this group has served its purpose.
+ // We are in: an invitation to this group has served its purpose, and a
+ // linking code shown before has been approved.
if (onJoined) onJoined(groupId);
+ deviceWaitRef.current = 0;
+ setNeedsDevice(false);
+ setDeviceCode('');
const indexMsg = await transport.fetchIndex();
if (cancelled) return;
@@ -680,7 +708,30 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// whole point of device linking: a second browser or a native client
// must not cost anyone a support request.
if (err.reason === 'unknown_device') setNeedsDevice(true);
- setError(err.message);
+ fileDeviceRef.current = false;
+ if (err.deviceCode) {
+ setDeviceCode(err.deviceCode);
+ deviceWaitRef.current = _untilSeconds(err.deviceExpiresAt)
+ || Date.now() / 1000 + 3600;
+ }
+ // A code is shown: the group opens by itself once a linked device
+ // approves it, as the command-line client goes on by itself. Tried
+ // again only once this try is over, so a slow connection is never cut
+ // short by the next; past the request's life the code is dropped and
+ // the button comes back for a new one.
+ if (deviceWaitRef.current && err.reason === 'unknown_device') {
+ if (Date.now() / 1000 < deviceWaitRef.current) {
+ deviceRetry = setTimeout(() => setRetryKey(k => k + 1), DEVICE_RETRY_MS);
+ } else {
+ deviceWaitRef.current = 0;
+ setDeviceCode('');
+ }
+ }
+ // In the application the linking form says all there is to say, and
+ // while a code is shown this is the refusal of each retry, not news.
+ const quiet = err.reason === 'unknown_device' && await platform.nativeKeys();
+ if (cancelled) return;
+ if (!quiet) setError(err.message);
setStatus('error');
if (transportRef.current) {
try { transportRef.current.close(); } catch { /* already gone */ }
@@ -704,6 +755,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
return () => {
cancelled = true;
if (offReconnect) { offReconnect(); offReconnect = null; }
+ clearTimeout(deviceRetry);
// Nothing will update this group's dock row once the page lets go of it.
reportIndexPush(groupId, null);
if (transportRef.current) {
@@ -967,17 +1019,15 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
<button class="admin-btn" style="margin-left:8px;font-size:0.9em"
onClick=${() => setRetryKey(k => k + 1)}>${t('group.retry')}</button>
</div>`}
- ${needsDevice && html`
+ ${needsDevice && nativeKeys && html`
<div class="invite-form" style="margin-bottom:12px">
<h4>${t('device.add_title')}</h4>
<p class="settings-hint">${t('device.add_hint')}</p>
${!deviceCode && html`
- <button class="admin-btn" onClick=${async () => {
- try {
- const transport = transportRef.current;
- const out = await transport.requestDeviceAdd(userId);
- setDeviceCode(out.code);
- } catch (err) { setError(err.message); }
+ <button class="admin-btn" onClick=${() => {
+ // The refused connection is closed: a new one files the request.
+ fileDeviceRef.current = true;
+ setRetryKey(k => k + 1);
}}>${t('device.add_btn')}</button>
`}
${deviceCode && html`
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js b/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
index 3d56f69..5c6adef 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport-devices.js
@@ -227,15 +227,24 @@ extendTransport(class {
const listed = await this._sendAndWait({ type: 'device_lookup', v: '0.1' });
if (listed.type === 'error') throw new Error(listed.detail || 'Not found');
- let match = null;
+ // Every match, not the first: a request is filed with the account's token
+ // alone, so somebody holding the passphrase who saw the code on screen can
+ // file a second one with the same code and keys of their own. Neither is
+ // approved then — the person's device asks again, with a new code.
+ const matches = [];
for (const req of listed.requests || []) {
const expect = await C.deviceCodeHash(
normalized, req.pk_ed25519, req.pk_x25519);
- if (expect === req.code_hash) { match = req; break; }
+ if (expect === req.code_hash) matches.push(req);
}
- if (!match) {
+ if (!matches.length) {
throw new Error('No device is waiting with that code');
}
+ if (matches.length > 1) {
+ throw new Error('More than one device is waiting with that code, so none was '
+ + 'approved. Get a new code on the device you are linking.');
+ }
+ const match = matches[0];
return this._countersign(userId, match.code_hash,
match.pk_ed25519, match.pk_x25519);
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index c86d668..a07f431 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -1178,6 +1178,21 @@ class MeshBayTransport {
} catch (e) {
// The UI turns this into "ask the operator for an invite code".
this._joinError = e;
+ // Asked for by the page ("Get a linking code"): filed on this same
+ // connection, still before the proof (MNP 6.3), because the page
+ // closes a connection that did not finish. Only for an identity the
+ // application keeps: one a browser minted is gone by its next
+ // connection, and approving it would link nothing.
+ if (e.reason === 'unknown_device' && this.fileDeviceRequest
+ && this._identity.native) {
+ try {
+ const filed = await this.requestDeviceAdd(userId);
+ e.deviceCode = filed.code;
+ e.deviceExpiresAt = filed.expiresAt;
+ } catch (fe) {
+ e.message = fe.message;
+ }
+ }
}
}