Checkpoint peering identity and opt-in discovery
This commit is contained in:
1 parent
eba9b50e30
commit
47de081721
44 files changed
+1557
-55
No files matched your search
+53
-16
@@ -127,11 +127,17 @@
|
||||
let pendingUrl = '';
|
||||
function connectionPanel(peer) {
|
||||
const remote = access.get(peer.id);
|
||||
const identityStatus = !peer.paired
|
||||
? peerState('pending', 'Identity not verified yet', 'Verify the other server after importing its invitation.')
|
||||
: remote?.error
|
||||
? peerState('error', 'Other server status unavailable', `This server is verified; refresh status to check the other side. ${remote.error}`)
|
||||
: remote?.paired
|
||||
? peerState('ok', 'Peering confirmed on both servers', 'Both servers have verified each other’s certificate.')
|
||||
: peerState('pending', 'Identity verified on this server', 'Waiting for the other server to verify this one.');
|
||||
const panel = h('section', { class: 'peer-step' }, h('h3', {}, 'Connection and identity'),
|
||||
h('p', { class: peer.paired ? 'peer-status-ok' : 'peer-status-pending' }, peer.paired ? 'This server has verified the peer certificate.' : 'Import the other server’s invitation before verifying it.'),
|
||||
identityStatus,
|
||||
h('p', { class: 'muted small' }, `Our fingerprint: ${state.identity?.fingerprint || 'certificate not created'}`),
|
||||
h('p', { class: 'muted small' }, `Their fingerprint: ${peer.fingerprint}`),
|
||||
h('p', { class: 'muted small' }, remote?.paired ? 'The other server has verified this server too.' : remote?.error ? `Other side: ${remote.error}` : 'The other server has not confirmed its side yet.'),
|
||||
peer.paired ? null : button('Verify the other server', () => command({ command: 'probe', id: peer.id }, 'Identity verified here. Ask the other admin to verify back.'), 'primary', busy),
|
||||
button('Refresh other-side status', () => refreshAccess(peer.id), '', busy));
|
||||
if (!peer.paired || !remote?.paired) {
|
||||
@@ -161,18 +167,31 @@
|
||||
const grid = panel.lastChild;
|
||||
const catalog = h('input', { type: 'checkbox', checked: (peer.grantScopes || []).includes('catalog') || (peer.grantScopes || []).includes('download') });
|
||||
const download = h('input', { type: 'checkbox', checked: (peer.grantScopes || []).includes('download') });
|
||||
catalog.onchange = () => { if (!catalog.checked) download.checked = false; };
|
||||
download.onchange = () => { if (download.checked) catalog.checked = true; };
|
||||
const grantHint = h('p', { class: 'muted small', role: 'status' });
|
||||
let grantAction = null;
|
||||
const updateGrantHint = () => {
|
||||
grantHint.textContent = !peer.paired
|
||||
? 'Granting is locked until identity pairing is complete. Finish the invitation exchange and verify this server above.'
|
||||
: (!catalog.checked && !download.checked ? 'Choose a permission above to enable Issue grant.' : '');
|
||||
grantHint.hidden = !grantHint.textContent;
|
||||
if (grantAction) grantAction.disabled = busy || !peer.paired || (!catalog.checked && !download.checked);
|
||||
};
|
||||
catalog.onchange = () => { if (!catalog.checked) download.checked = false; updateGrantHint(); };
|
||||
download.onchange = () => { if (download.checked) catalog.checked = true; updateGrantHint(); };
|
||||
updateGrantHint();
|
||||
const token = h('textarea', { rows: '3', readonly: true, placeholder: 'Issue a grant to create a one-time token.', 'aria-label': 'Grant token' }, pendingGrant?.id === peer.id ? pendingGrant.token : '');
|
||||
grantAction = peer.grantStatus === 'none' || peer.grantStatus === 'expired'
|
||||
? button('Issue grant', async () => { const result = await command({ command: 'grant', id: peer.id,
|
||||
scopes: [...(catalog.checked ? ['catalog'] : []), ...(download.checked ? ['download'] : [])] }, 'Grant issued; send the token privately.');
|
||||
if (result?.grant) { pendingGrant = { id: peer.id, token: result.grant.token }; draw(); } }, 'primary', !peer.paired || (!catalog.checked && !download.checked) || busy)
|
||||
: button('Save grant permissions', () => command({ command: 'set', id: peer.id, patch: { grantScopes: [...(catalog.checked ? ['catalog'] : []), ...(download.checked ? ['download'] : [])] } }, 'Grant permissions updated'), '', !peer.paired || (!catalog.checked && !download.checked) || busy);
|
||||
updateGrantHint();
|
||||
const outbound = h('section', { class: 'peer-step' }, h('h4', {}, 'We grant access to ', peerTag(peer.alias)),
|
||||
h('p', { class: 'muted small' }, `Status: ${peer.grantStatus}. Expires: ${date(peer.grantExpiresAt)}. Pausing preserves the grant; revoke only when you want to invalidate it.`),
|
||||
localGrantStatus(peer),
|
||||
h('label', { class: 'check' }, catalog, h('span', {}, 'Browse approved catalog')),
|
||||
h('label', { class: 'check' }, download, h('span', {}, 'Download chapters (includes catalog access)')),
|
||||
peer.grantStatus === 'none' || peer.grantStatus === 'expired'
|
||||
? button('Issue grant', async () => { const result = await command({ command: 'grant', id: peer.id,
|
||||
scopes: [...(catalog.checked ? ['catalog'] : []), ...(download.checked ? ['download'] : [])] }, 'Grant issued; send the token privately.');
|
||||
if (result?.grant) { pendingGrant = { id: peer.id, token: result.grant.token }; draw(); } }, 'primary', !peer.paired || (!catalog.checked && !download.checked) || busy)
|
||||
: button('Save grant permissions', () => command({ command: 'set', id: peer.id, patch: { grantScopes: [...(catalog.checked ? ['catalog'] : []), ...(download.checked ? ['download'] : [])] } }, 'Grant permissions updated'), '', (!catalog.checked && !download.checked) || busy),
|
||||
grantHint,
|
||||
grantAction,
|
||||
peer.grantStatus === 'active' ? button('Pause access', () => command({ command: 'set', id: peer.id, patch: { servingPaused: true } }, 'Access paused; grant retained'))
|
||||
: peer.grantStatus === 'paused' ? button('Resume access', () => command({ command: 'set', id: peer.id, patch: { servingPaused: false } }, 'Access resumed')) : null,
|
||||
field('Grant token · copy once and send privately', token),
|
||||
@@ -182,7 +201,7 @@
|
||||
pendingGrant = null; await command({ command: 'revoke-grant', id: peer.id }, 'Grant revoked.');
|
||||
}, 'danger small') : null);
|
||||
const inbound = h('section', { class: 'peer-step' }, h('h4', {}, peerTag(peer.alias), ' grants access to us'),
|
||||
h('p', { class: 'muted small' }, remoteGrantText(access.get(peer.id))),
|
||||
remoteGrantStatus(access.get(peer.id)),
|
||||
h('label', { class: 'check' }, h('input', { type: 'checkbox', checked: !!peer.receive, disabled: !peer.hasGrant,
|
||||
onchange: event => command({ command: 'set', id: peer.id, patch: { receive: event.target.checked } }, 'Local download permission saved') }),
|
||||
h('span', {}, 'Allow this server to download from them')),
|
||||
@@ -200,13 +219,31 @@
|
||||
h('span', {}, 'Let ordinary readers queue downloads')));
|
||||
grid.append(outbound, inbound); return panel;
|
||||
}
|
||||
function remoteGrantText(value) {
|
||||
if (!value) return 'Refresh to ask the other server for its current access status.';
|
||||
if (value.error) return `Status unavailable: ${value.error}`;
|
||||
function peerState(tone, title, detail) {
|
||||
return h('div', { class: `peer-state peer-state-${tone}`, role: 'status', 'aria-live': 'polite' },
|
||||
h('strong', {}, title), detail ? h('span', {}, detail) : null);
|
||||
}
|
||||
function localGrantStatus(peer) {
|
||||
const scopes = peer.grantScopes || [];
|
||||
const access = scopes.includes('download') ? 'Browse and download chapters' : scopes.includes('catalog') ? 'Browse approved catalog' : '';
|
||||
if (peer.grantStatus === 'active') return peerState('ok', 'Grant active', `${access} · expires ${date(peer.grantExpiresAt)}.`);
|
||||
if (peer.grantStatus === 'paused') return peerState('pending', 'Grant paused', 'The grant is retained. Resume access to make it usable again.');
|
||||
if (peer.grantStatus === 'expired') return peerState('warn', 'Grant expired', 'Issue a new grant to restore access.');
|
||||
return peerState('neutral', 'No grant issued', 'Choose the access level below to create a grant.');
|
||||
}
|
||||
function remoteGrantStatus(value) {
|
||||
if (!value) return peerState('pending', 'Status not checked', 'Refresh to ask the other server for its current access status.');
|
||||
if (value.error) return peerState('error', 'Could not confirm access', value.error);
|
||||
const grant = value.grant || {};
|
||||
const scopes = grant.scopes || [];
|
||||
const role = scopes.includes('download') ? 'Browse + download' : scopes.includes('catalog') ? 'Browse catalog' : 'No access grant';
|
||||
return `${value.paired ? 'Identity verified' : 'The other server has not verified us'} · ${role} · ${grant.status || 'unknown'} · expires ${date(grant.expiresAt)}`;
|
||||
if (!value.paired) return peerState('pending', 'Identity not verified by peer', 'Ask the other admin to verify this server.');
|
||||
if (grant.status === 'active') {
|
||||
const role = scopes.includes('download') ? 'Browse and download chapters' : 'Browse approved catalog';
|
||||
return peerState('ok', 'Active grant from peer', `${role} · expires ${date(grant.expiresAt)}.`);
|
||||
}
|
||||
if (grant.status === 'paused') return peerState('pending', 'Peer grant paused', 'The other admin can resume it without exchanging a new token.');
|
||||
if (grant.status === 'expired') return peerState('warn', 'Peer grant expired', 'Ask the other admin to issue a new grant.');
|
||||
return peerState('neutral', 'No active grant from peer', 'The other server has not granted catalog or download access.');
|
||||
}
|
||||
function limitPanel(peer) {
|
||||
const limits = peer.limits || {}, download = peer.download || {};
|
||||
|
||||
Reference in new issue
Block a user