mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-21 16:36:44 +00:00
feat(chat): add encrypted invite contract
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
(function exposeInviteLinks(root) {
|
||||
const J2_PREFIX = '#j2:';
|
||||
const LEGACY_PREFIX = '#join:';
|
||||
|
||||
function parseJ2Hash(hash) {
|
||||
if (typeof hash !== 'string' || !hash.startsWith(J2_PREFIX)) return null;
|
||||
const params = new URLSearchParams(hash.slice(J2_PREFIX.length));
|
||||
const roomId = params.get('r') || '';
|
||||
const password = params.get('p') || '';
|
||||
const chatKey = params.get('k') || '';
|
||||
const serverUrl = params.get('u') || '';
|
||||
if (!roomId || !password || !chatKey) return null;
|
||||
return {
|
||||
format: 'j2',
|
||||
roomId,
|
||||
password,
|
||||
chatKey,
|
||||
useCustomServer: serverUrl.length > 0,
|
||||
serverUrl
|
||||
};
|
||||
}
|
||||
|
||||
function parseLegacyHash(hash) {
|
||||
if (typeof hash !== 'string' || !hash.startsWith(LEGACY_PREFIX)) return null;
|
||||
const parts = hash.slice(LEGACY_PREFIX.length).split(':');
|
||||
const roomId = parts.shift() || '';
|
||||
let useCustomServer = false;
|
||||
let serverUrl = '';
|
||||
|
||||
if (parts.length >= 3) {
|
||||
const flag = parts.at(-2);
|
||||
const rawUrl = parts.at(-1) || '';
|
||||
let decodedUrl = '';
|
||||
try {
|
||||
decodedUrl = decodeURIComponent(rawUrl);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
const custom = flag === '1' && /^(?:ws|wss):\/\//.test(decodedUrl);
|
||||
const official = flag === '0' && rawUrl === '';
|
||||
if (custom || official) {
|
||||
parts.splice(-2, 2);
|
||||
useCustomServer = custom;
|
||||
serverUrl = custom ? decodedUrl : '';
|
||||
}
|
||||
}
|
||||
|
||||
const password = parts.join(':');
|
||||
if (!roomId || !password) return null;
|
||||
return {
|
||||
format: 'legacy',
|
||||
roomId,
|
||||
password,
|
||||
chatKey: '',
|
||||
useCustomServer,
|
||||
serverUrl
|
||||
};
|
||||
}
|
||||
|
||||
function parseInviteHash(hash) {
|
||||
return parseJ2Hash(hash) || parseLegacyHash(hash);
|
||||
}
|
||||
|
||||
function buildJ2Hash({ roomId, password, chatKey, serverUrl = '' }) {
|
||||
if (!roomId || !password || !chatKey) throw new TypeError('roomId, password, and chatKey are required');
|
||||
const params = new URLSearchParams({ r: roomId, p: password, k: chatKey });
|
||||
if (serverUrl) params.set('u', serverUrl);
|
||||
return `${J2_PREFIX}${params.toString()}`;
|
||||
}
|
||||
|
||||
root.KoalaSyncInviteLinks = Object.freeze({
|
||||
J2_PREFIX,
|
||||
LEGACY_PREFIX,
|
||||
parseInviteHash,
|
||||
buildJ2Hash
|
||||
});
|
||||
})(globalThis);
|
||||
@@ -0,0 +1,58 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
beforeAll(async () => {
|
||||
await import('./invite-links.js');
|
||||
});
|
||||
|
||||
describe('invite links', () => {
|
||||
it('round-trips official and custom j2 links', () => {
|
||||
const api = globalThis.KoalaSyncInviteLinks;
|
||||
const official = api.buildJ2Hash({
|
||||
roomId: 'SAPPHIRE-DUCK-49',
|
||||
password: '0X:UK3C',
|
||||
chatKey: 'R5Ti1nxp0crfAFHf3gVncw'
|
||||
});
|
||||
expect(api.parseInviteHash(official)).toEqual({
|
||||
format: 'j2',
|
||||
roomId: 'SAPPHIRE-DUCK-49',
|
||||
password: '0X:UK3C',
|
||||
chatKey: 'R5Ti1nxp0crfAFHf3gVncw',
|
||||
useCustomServer: false,
|
||||
serverUrl: ''
|
||||
});
|
||||
|
||||
const custom = api.buildJ2Hash({
|
||||
roomId: 'SILENT-EAGLE-90',
|
||||
password: '30PXPD',
|
||||
chatKey: 'R5Ti1nxp0crfAFHf3gVncw',
|
||||
serverUrl: 'wss://sync.example.test/socket?region=eu'
|
||||
});
|
||||
expect(api.parseInviteHash(custom)).toMatchObject({
|
||||
useCustomServer: true,
|
||||
serverUrl: 'wss://sync.example.test/socket?region=eu'
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps legacy links compatible, including colons in passwords', () => {
|
||||
const parse = globalThis.KoalaSyncInviteLinks.parseInviteHash;
|
||||
expect(parse('#join:ROOM-1:pass:with:colons')).toMatchObject({
|
||||
format: 'legacy',
|
||||
password: 'pass:with:colons',
|
||||
chatKey: '',
|
||||
useCustomServer: false
|
||||
});
|
||||
expect(parse('#join:ROOM-1:pass:with:colons:1:wss%3A%2F%2Frelay.example')).toMatchObject({
|
||||
password: 'pass:with:colons',
|
||||
useCustomServer: true,
|
||||
serverUrl: 'wss://relay.example'
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects incomplete or malformed invite hashes', () => {
|
||||
const parse = globalThis.KoalaSyncInviteLinks.parseInviteHash;
|
||||
expect(parse('#j2:r=ROOM&p=PASS')).toBeNull();
|
||||
expect(parse('#join:ROOM')).toBeNull();
|
||||
expect(parse('#join:ROOM:PASS:1:%E0%A4%A')).toBeNull();
|
||||
expect(parse('#other:r=ROOM&p=PASS&k=KEY')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user