here goes nothin'

This commit is contained in:
legop3
2026-06-27 04:23:58 -04:00
parent 0172ea5150
commit 3c1b1dab6e
21 changed files with 1554 additions and 332 deletions
+7
View File
@@ -8,6 +8,7 @@
"name": "webui",
"version": "0.0.0",
"dependencies": {
"@thumbmarkjs/thumbmarkjs": "^1.10.0",
"midi-file": "^1.2.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
@@ -1417,6 +1418,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@thumbmarkjs/thumbmarkjs": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@thumbmarkjs/thumbmarkjs/-/thumbmarkjs-1.10.0.tgz",
"integrity": "sha512-mmIjivwI76Jm1to/VsEzxX+FNRD38HBHV37LmlF6Gg6psclifkVO01KV7UZr+i2d9IboqsTcW33vD1vr3meCeA==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+1
View File
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@thumbmarkjs/thumbmarkjs": "^1.10.0",
"midi-file": "^1.2.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+2 -2
View File
@@ -337,8 +337,8 @@ export function SessionProvider({ children }) {
const actions = useMemo(
() => ({
login: (username, password) => emitWithAck('auth:login', { username, password }),
identifySession: ({ cookieUserId, nickname, overseerEnabled, identitySurface } = {}) =>
emitWithAck('session:identify', { cookieUserId, nickname, overseerEnabled, identitySurface }),
identifySession: ({ cookieUserId, fingerprintId, nickname, overseerEnabled, identitySurface } = {}) =>
emitWithAck('session:identify', { cookieUserId, fingerprintId, nickname, overseerEnabled, identitySurface }),
setRole: (role) => emitWithAck('session:setRole', { role }),
requestControl: (roverId, options = {}) =>
emitWithAck('session:requestControl', { roverId, ...options }),
+11
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useRef } from 'react';
import { useSessionActions, useSessionSelector } from '../context/SessionContext.jsx';
import { useSocket } from '../context/SocketContext.jsx';
import { getBrowserFingerprintId } from '../lib/browserFingerprint.js';
import { useSettingsNamespace } from '../settings/index.js';
export default function useUserIdentitySync({ identitySurface = 'passive' } = {}) {
@@ -21,6 +22,7 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
const inFlightRef = useRef(false);
const lastAckSocketRef = useRef(null);
const retryTimerRef = useRef(null);
const fingerprintRef = useRef('');
const ready =
identityStatus === 'ready' && profileStatus === 'ready' && overseerPreferenceStatus === 'ready';
@@ -40,6 +42,14 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
if (!ready || !connected || !socket?.id || inFlightRef.current) return;
inFlightRef.current = true;
try {
if (!fingerprintRef.current) {
/*
The portable cookie key is still the cross-device identity signal.
Thumbmark adds a same-device signal that survives cookie clearing, so
both are sent together whenever the heartbeat identifies this socket.
*/
fingerprintRef.current = await getBrowserFingerprintId();
}
/*
Every route shares the same persisted identity key, but only the main
driver page should trigger duplicate-tab prevention. Sending the surface
@@ -48,6 +58,7 @@ export default function useUserIdentitySync({ identitySurface = 'passive' } = {}
*/
const resp = await identifySession({
cookieUserId,
fingerprintId: fingerprintRef.current,
nickname,
overseerEnabled,
identitySurface: normalizedIdentitySurface,
+34
View File
@@ -0,0 +1,34 @@
// Browser Fingerprint
// Purpose: Wraps Thumbmark behind one app-owned helper so identity sync does not
// depend on the third-party package shape throughout the UI.
// Scope: Produces a normalized device fingerprint signal for the server identity service.
import { getFingerprint } from '@thumbmarkjs/thumbmarkjs';
let fingerprintPromise = null;
function normalizeThumbmark(value) {
const raw = String(value || '').trim().toLowerCase();
if (!raw) return '';
/*
The server treats the prefix as part of the signal format so different
fingerprint providers can coexist later without hash-space ambiguity.
*/
const body = raw.startsWith('tm_') ? raw.slice(3) : raw;
const safeBody = body.replace(/[^a-z0-9_-]/g, '');
return safeBody ? `tm_${safeBody}` : '';
}
export async function getBrowserFingerprintId() {
if (typeof window === 'undefined') return '';
if (!fingerprintPromise) {
fingerprintPromise = Promise.resolve()
.then(() => getFingerprint())
.then(normalizeThumbmark)
.catch((error) => {
console.warn('Failed to calculate browser fingerprint', error); // eslint-disable-line no-console
return '';
});
}
return fingerprintPromise;
}