diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-page.js | 71 |
1 files changed, 48 insertions, 23 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 2ee6e05..fe858b2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -9,6 +9,7 @@ import { HUB, session, hubFetch, ensureFreshToken, _loadBundleKey, _loadRecoveryKey, _storeBundleKey, } from './hub-client.js'; +import * as platform from './platform.js'; import { APPS, visibleApps } from './apps.js'; import { GroupName } from './group-name.js'; import { useStickyBand } from './sticky.js'; @@ -16,8 +17,9 @@ import { FilePreview } from './files-app.js'; import { lazy } from './lazy.js'; // Fetched the first time a video is played / the Settings tab is opened. +const playerFrame = (content) => html`<div class="video-overlay video-player-overlay">${content}</div>`; const VideoPlayer = lazy(() => import('./video-player.js'), 'VideoPlayer', - html`<div class="video-overlay"><p class="page-message"><span class="spinner"></span></p></div>`); + playerFrame(html`<p class="page-message"><span class="spinner"></span></p>`), playerFrame); const GroupSettingsPanel = lazy(() => import('./group-settings.js'), 'GroupSettingsPanel'); import { reportIndexPush } from './index-dock.js'; import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js'; @@ -201,6 +203,17 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // This browser holds a key the node does not know, for an account it does. // Not the operator's problem: a device already paired here can admit it. const [needsDevice, setNeedsDevice] = useState(false); + // In a browser, for an account whose identities the desktop application + // keeps to itself: said when this group cannot be opened here, so the way + // in is named — the application — instead of a code nobody can use. Starts + // as "native" so the application never flashes a notice about itself. + const [nativeKeys, setNativeKeys] = useState(true); + useEffect(() => { + let gone = false; + platform.nativeKeys().then((n) => { if (!gone) setNativeKeys(n); }, () => {}); + return () => { gone = true; }; + }, []); + const browserAccessOff = !nativeKeys && userPrefs && userPrefs.browser_access === 'off'; const [deviceCode, setDeviceCode] = useState(''); const [codeInput, setCodeInput] = useState(''); // This browser has never derived the passphrase-bundle key (fresh browser, @@ -244,12 +257,13 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, setError(''); try { // Same derivation as sign-in — the token is already ours, only the key - // that opens node bundles is missing here. Persisted so this browser is - // set up from now on. - session.bundleKey = { - ...(await window.MeshBayKeys.bundleKeyPairFields(pass, username)), - v1: await window.MeshBayKeys.deriveEncryptionKeyV1(pass, username), - }; + // that opens node bundles is missing here. The hub hands the pepper that + // goes into it only against the passphrase proof, never the token alone. + // Persisted so this browser is set up from now on. + const authKey = await window.MeshBayKeys.deriveAuthKey(pass, username); + const { pepper, version } = await window.MeshBayKeys.fetchBundlePepper(token, authKey); + session.bundleKey = await window.MeshBayKeys.sessionBundleKey( + pass, username, userId, pepper, version); await _storeBundleKey(session.bundleKey); setPassInput(''); setNeedsPass(false); @@ -259,7 +273,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, } finally { setPassBusy(false); } - }, [passInput, username]); + }, [passInput, username, userId, token]); // Everything one handshake ack tells this page, applied in one place. // @@ -379,7 +393,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // No keys are carried in: the transport fetches this node's identity // from the node, or creates one there on a first join. - const sessionKeys = null; + const identity = null; setStatus('connecting'); // Renewed here rather than taken from the prop. This effect no longer @@ -432,8 +446,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, }; try { ack = await transport.connect( - n.node_id, live, groupId, null, sessionKeys, session.bundleKey, - username, userId, joinCode, session.recoveryKey, joinNodePk); + n.node_id, live, groupId, null, identity, session.bundleKey, + username, userId, joinCode, session.recoveryKey, joinNodePk, n.pk_node); break; } catch (e) { lastErr = e; @@ -508,18 +522,15 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, }; setOperatorPaired(transport.memberRole === 'operator'); - // A first join to this node generated an identity for it; leave it with - // the node so any other browser can become the same person here with the - // passphrase. It is this node's key and no other's. - if (transport.connected && transport.newNodeBundle) { - try { - await transport.storeKeypairBundle( - transport.newNodeBundle, transport.newNodeBundleRecovery); - transport.newNodeBundle = null; - transport.newNodeBundleRecovery = null; - } catch (e) { - console.warn('[MeshBay] could not leave our key with the node:', e.message); - } + // What this node should hold of our identity: the bundle of one just + // created, so another browser can become the same person here — or, in + // the desktop application, whatever the account's browser access says. + // Not awaited: nothing on this page depends on it, and the group opens + // without waiting for a round trip to the node about a backup. + if (transport.connected) { + transport.settleNodeBundle().catch((e) => { + console.warn('[MeshBay] could not settle our key with the node:', e.message); + }); } // Import GEK from transport (fetched from node during handshake) @@ -649,6 +660,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // The node has no bundle for us and this browser derived no key to make // one — the passphrase form below is the way in, not a support request. if (err.reason === 'no_keys') setNeedsPass(true); + // An identity sealed before the pepper: only the operator can clear it. + if (err.reason === 'bundle_format_retired') err.message = t('group.bundle_format_retired'); // A key this node has never pinned, for an account it knows. The way in // is a device already trusted here, not an operator — which is the // whole point of device linking: a second browser or a native client @@ -833,8 +846,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, }), [perAppDirectories, chatDirectory, chatLinkPreview, tmdbConfig, musicbrainzConfig]); + // Reporting a file is offered in a public group only: that is the only place + // the hub's moderation reaches (docs/MESHBAY_DESIGN.md §7.5), and the hub + // refuses a report from anyone who is not a member of the group named here. + const isPublic = !!(group && group.visibility === 'public'); + const reportContent = useCallback((contentHash, reason, detail) => hubFetch( + '/v1/reports', { method: 'POST', token, + body: { content_hash: contentHash, group_id: groupId, reason, detail } }), + [groupId, token]); + const commonProps = { groupId, transportRef, gekRef, status, username, deviceReady, + onReport: isPublic ? reportContent : null, entries, availableEntries, nodeDirs, nodeRoots, setEntries, setNodeDirs, setNodeRoots, applyIndex, isNodeAdmin, operatorPaired, attachRoot, attachDir, userId, setError, onPreview, @@ -895,6 +918,8 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, </button> `} </div> + ${browserAccessOff && (error || needsDevice || needsCode || needsPass) && html` + <p class="settings-hint" style="margin-bottom:12px">${t('group.browser_access_off')}</p>`} ${error && html`<div class="error-msg" style="margin-bottom:12px">${error}${' '} <button class="admin-btn" style="margin-left:8px;font-size:0.9em" onClick=${() => setRetryKey(k => k + 1)}>${t('group.retry')}</button> |