cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

main
goq / portal / enrollment.mjs
enrollment.mjs 3.1K
1const NODE_ID_PATTERN = /^[0-9a-f]{64}$/;2const GRANTS = Object.freeze(['view', 'pointer_keyboard', 'gamepad']);34export function shortPeerFingerprint(nodeId) {5 if (typeof nodeId !== 'string' || !NODE_ID_PATTERN.test(nodeId)) {6 throw new TypeError('node ID must be 64 lowercase hexadecimal characters');7 }8 return `${nodeId.slice(0, 8)}…${nodeId.slice(-8)}`;9}1011export function normalizeInvitationSummary(value, nowUnix = Math.floor(Date.now() / 1000)) {12 if (!value || typeof value !== 'object' || Array.isArray(value)) {13 throw new TypeError('invitation summary must be an object');14 }15 const allowed = new Set([16 'host_node_id',17 'peer_node_id',18 'expires_at_unix',19 'grants',20 'view_only',21 'input_capable',22 ]);23 for (const key of Object.keys(value)) {24 if (!allowed.has(key)) throw new TypeError(`unknown invitation summary field: ${key}`);25 }26 if (!NODE_ID_PATTERN.test(value.host_node_id || '')) throw new TypeError('invalid host node ID');27 if (!NODE_ID_PATTERN.test(value.peer_node_id || '')) throw new TypeError('invalid peer node ID');28 if (!Number.isSafeInteger(value.expires_at_unix) || value.expires_at_unix <= nowUnix) {29 throw new TypeError('invitation is expired or has an invalid expiry');30 }31 if (!Array.isArray(value.grants) || value.grants.length === 0 || value.grants.length > GRANTS.length) {32 throw new TypeError('invitation grants must be a non-empty bounded list');33 }34 const grants = [];35 for (const grant of value.grants) {36 if (!GRANTS.includes(grant) || grants.includes(grant)) {37 throw new TypeError('invitation contains an unknown or duplicate grant');38 }39 grants.push(grant);40 }41 const inputCapable = grants.includes('pointer_keyboard') || grants.includes('gamepad');42 if (value.input_capable !== undefined && value.input_capable !== inputCapable) {43 throw new TypeError('invitation input eligibility conflicts with its grants');44 }45 if (value.view_only !== undefined && value.view_only !== !inputCapable) {46 throw new TypeError('invitation view-only state conflicts with its grants');47 }48 return Object.freeze({49 hostNodeId: value.host_node_id,50 peerNodeId: value.peer_node_id,51 hostFingerprint: shortPeerFingerprint(value.host_node_id),52 peerFingerprint: shortPeerFingerprint(value.peer_node_id),53 expiresAtUnix: value.expires_at_unix,54 grants: Object.freeze(grants),55 viewOnly: !inputCapable,56 inputCapable,57 });58}5960export function grantLabel(grant) {61 switch (grant) {62 case 'view': return 'view stream';63 case 'pointer_keyboard': return 'keyboard + mouse';64 case 'gamepad': return 'gamepad';65 default: throw new TypeError(`unknown invitation grant: ${grant}`);66 }67}6869export function formatInvitationExpiry(expiresAtUnix, nowUnix = Math.floor(Date.now() / 1000)) {70 if (!Number.isSafeInteger(expiresAtUnix) || !Number.isSafeInteger(nowUnix)) {71 throw new TypeError('expiry values must be integer Unix seconds');72 }73 const remaining = expiresAtUnix - nowUnix;74 if (remaining <= 0) return 'expired';75 if (remaining < 120) return 'expires in under 2 minutes';76 const minutes = Math.ceil(remaining / 60);77 return `expires in ${minutes} minutes`;78}