aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
diff options
context:
space:
mode:
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.js69
1 files changed, 47 insertions, 22 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 eb012c3..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,7 +446,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
};
try {
ack = await transport.connect(
- n.node_id, live, groupId, null, sessionKeys, session.bundleKey,
+ n.node_id, live, groupId, null, identity, session.bundleKey,
username, userId, joinCode, session.recoveryKey, joinNodePk, n.pk_node);
break;
} catch (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>