repo/services/multistreaming/test/security.test.js
Ezequiel C. 187379de4e multistreaming: scenes/composition, installer build support, service updates
- multistreaming (new): RTMP ingest + multi-platform fan-out with pluggable providers (Twitch/YouTube/Kick/custom), zero-knowledge key vaults, Authelia OIDC auth, shared rooms with editor/streamer roles, single-use invites, per-account streaming grants, and scenes & composition (grid/PiP layouts, text/image overlays, per-output audio routing).
- installer: support Dockerfile build in metadata (not just image) and RSA key generation for the Authelia OIDC JWKS.
- authelia: add OIDC provider with portainer + multistreaming clients (public + PKCE).
- services: remove allprox; add nginx-proxy-manager and portainer; update lldap; regenerate catalog.
2026-09-02 21:18:25 +02:00

75 lines
3.2 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
test('invite tokens are 256-bit (64 hex chars)', () => {
const { Store } = require('../src/store');
const fs = require('fs');
const os = require('os');
const path = require('path');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ms-sec-'));
const store = new Store(dir);
const owner = store.createUser({ username: 'a', passwordHash: 'x', passwordSalt: 'y' });
const room = store.createRoom({ name: 'R', ownerId: owner.id });
const invite = store.createInvite({ roomId: room.id, role: 'streamer' });
// 32 bytes → 64 hex chars.
assert.equal(invite.token.length, 64);
assert.match(invite.token, /^[0-9a-f]{64}$/);
// Expiry is set in the future.
assert.ok(invite.expiresAt > Date.now());
fs.rmSync(dir, { recursive: true, force: true });
});
test('invite is single-use (consumed on accept)', () => {
const { Store } = require('../src/store');
const fs = require('fs');
const os = require('os');
const path = require('path');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ms-sec-'));
const store = new Store(dir);
const owner = store.createUser({ username: 'a', passwordHash: 'x', passwordSalt: 'y' });
const bob = store.createUser({ username: 'b', passwordHash: 'x', passwordSalt: 'y' });
const room = store.createRoom({ name: 'R', ownerId: owner.id });
const invite = store.createInvite({ roomId: room.id, role: 'editor' });
assert.ok(store.findInviteByToken(invite.token));
store.acceptInvite(invite, bob.id);
// Consumed: a second lookup fails, and a third user can't reuse it.
assert.equal(store.findInviteByToken(invite.token), null);
fs.rmSync(dir, { recursive: true, force: true });
});
test('redactUrl hides the stream key', () => {
// redactUrl is not exported; test the behavior through a fresh copy of the logic.
function redactUrl(url) {
const idx = url.lastIndexOf('/');
if (idx <= 0) return url;
return `${url.slice(0, idx)}/•••`;
}
assert.equal(redactUrl('rtmp://live.twitch.tv/app/live_abc123'), 'rtmp://live.twitch.tv/app/•••');
assert.ok(!redactUrl('rtmp://live.twitch.tv/app/live_abc123').includes('live_abc123'));
});
test('account secret ciphertext is never stored plaintext and key never in logs', () => {
const { Store } = require('../src/store');
const fs = require('fs');
const os = require('os');
const path = require('path');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ms-sec-'));
const store = new Store(dir);
const owner = store.createUser({ username: 'a', passwordHash: 'x', passwordSalt: 'y' });
const vault = store.createVault({ ownerId: owner.id, name: 'V', salt: 's', serverWrapped: { iv: 'i', data: 'd' } });
store.createAccount({
ownerId: owner.id,
vaultId: vault.id,
provider: 'twitch',
name: 'T',
url: 'rtmp://live.twitch.tv/app',
secretCiphertext: { iv: 'iv-here', data: 'ct-here' },
});
const persisted = JSON.stringify(store.state);
assert.ok(!persisted.includes('live_secret_key'), 'plaintext key must never be persisted');
assert.ok(persisted.includes('ct-here'), 'only ciphertext is stored');
fs.rmSync(dir, { recursive: true, force: true });
});