roomsync 0.0.0-stage → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -2
- package/THIRD_PARTY_NOTICES.txt +11008 -0
- package/bin/roomsync.cjs +7 -0
- package/lib/cli.cjs +111 -0
- package/lib/verify.cjs +56 -0
- package/native/manifest.json +10 -0
- package/native/win32-x64/roomsync-desktop.exe +0 -0
- package/package.json +32 -4
package/bin/roomsync.cjs
ADDED
package/lib/cli.cjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
const { verifyPackage } = require('./verify.cjs');
|
|
6
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
function parseArgs(args) {
|
|
9
|
+
const [command, ...rest] = args;
|
|
10
|
+
if (!command || ['help', '--help', '-h'].includes(command)) {
|
|
11
|
+
if (rest.length) throw new Error('Unexpected help arguments. Run roomsync --help.');
|
|
12
|
+
return { kind: 'help' };
|
|
13
|
+
}
|
|
14
|
+
if (['--version', '-v'].includes(command) && rest.length === 0) return { kind: 'version' };
|
|
15
|
+
if (command === 'doctor' && rest.length === 0) return { kind: 'doctor' };
|
|
16
|
+
if (command === 'devices' && rest.length === 0) return { kind: 'native', args: ['list-devices'] };
|
|
17
|
+
if (command === 'start') {
|
|
18
|
+
if (rest.length === 0) return { kind: 'native', args: ['web'] };
|
|
19
|
+
if (rest.length === 1 && rest[0] === '--no-browser') return { kind: 'native', args: ['web-headless'] };
|
|
20
|
+
if (rest.length === 1 && ['--help', '-h'].includes(rest[0])) return { kind: 'help' };
|
|
21
|
+
throw new Error('Use roomsync start or roomsync start --no-browser. Other options are not supported.');
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`Unknown command or extra arguments. Run roomsync --help.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function help(version) {
|
|
27
|
+
return `RoomSync ${version} · Windows host preview\n\n` +
|
|
28
|
+
' roomsync start Open the local setup/control page\n' +
|
|
29
|
+
' roomsync start --no-browser Print its address without opening a browser\n' +
|
|
30
|
+
' roomsync devices List Windows output endpoints\n' +
|
|
31
|
+
' roomsync doctor Check platform and bundled-host integrity\n' +
|
|
32
|
+
' roomsync --version\n\n' +
|
|
33
|
+
'Requires Windows 10 1703+ x64 and Node.js 20+. macOS, Linux, WSL and native\n' +
|
|
34
|
+
'Windows ARM64 are not supported by this package. Run from Windows PowerShell\n' +
|
|
35
|
+
'or Command Prompt, not WSL. Keep the terminal open; Ctrl+C stops the host.\n\n' +
|
|
36
|
+
'Start opens controls on 127.0.0.1 only. Audio sharing starts only after you\n' +
|
|
37
|
+
'create a private setup code and choose Start listening in the control page.\n' +
|
|
38
|
+
'Default capture does not delay or mute original host audio. Optional synced\n' +
|
|
39
|
+
'Windows speakers require explicit virtual routing. This unsigned preview\n' +
|
|
40
|
+
'still needs physical audio/synchronization validation. Timing diagnostics\n' +
|
|
41
|
+
'do not prove the physical cause of silence or cutouts.\n';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runNative(executable, args, options) {
|
|
45
|
+
const { spawnImpl, processImpl, platform, stderr } = options;
|
|
46
|
+
return new Promise(resolve => {
|
|
47
|
+
let child;
|
|
48
|
+
let settled = false;
|
|
49
|
+
const onInterrupt = () => {
|
|
50
|
+
// Windows delivers console Ctrl+C to the inherited native console process.
|
|
51
|
+
// Node child.kill('SIGINT') force-terminates Windows processes, so do not use it.
|
|
52
|
+
if (platform !== 'win32') child.kill('SIGINT');
|
|
53
|
+
};
|
|
54
|
+
const onTerminate = () => { child.kill('SIGTERM'); };
|
|
55
|
+
const onExit = () => { if (!settled && child.exitCode === null) child.kill(); };
|
|
56
|
+
const finish = code => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
settled = true;
|
|
59
|
+
processImpl.removeListener('SIGINT', onInterrupt);
|
|
60
|
+
processImpl.removeListener('SIGTERM', onTerminate);
|
|
61
|
+
processImpl.removeListener('exit', onExit);
|
|
62
|
+
resolve(code);
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
child = spawnImpl(executable, args, {
|
|
66
|
+
shell: false, stdio: 'inherit', windowsHide: false, detached: false
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
stderr.write(`RoomSync could not start: ${error.message}\n`);
|
|
70
|
+
finish(1); return;
|
|
71
|
+
}
|
|
72
|
+
processImpl.on('SIGINT', onInterrupt);
|
|
73
|
+
processImpl.on('SIGTERM', onTerminate);
|
|
74
|
+
processImpl.on('exit', onExit);
|
|
75
|
+
child.once('error', error => {
|
|
76
|
+
stderr.write(`RoomSync could not start its bundled host: ${error.message}\n`);
|
|
77
|
+
finish(1);
|
|
78
|
+
});
|
|
79
|
+
child.once('close', (code, signal) => {
|
|
80
|
+
finish(Number.isInteger(code) ? code : signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function run(args, options = {}) {
|
|
86
|
+
const {
|
|
87
|
+
root = ROOT, platform = process.platform, arch = process.arch,
|
|
88
|
+
stdout = process.stdout, stderr = process.stderr,
|
|
89
|
+
spawnImpl = spawn, processImpl = process
|
|
90
|
+
} = options;
|
|
91
|
+
let parsed;
|
|
92
|
+
try { parsed = parseArgs(args); }
|
|
93
|
+
catch (error) { stderr.write(`RoomSync: ${error.message}\n`); return 2; }
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
95
|
+
if (parsed.kind === 'help') { stdout.write(help(pkg.version)); return 0; }
|
|
96
|
+
if (parsed.kind === 'version') { stdout.write(`${pkg.version}\n`); return 0; }
|
|
97
|
+
const supported = platform === 'win32' && arch === 'x64';
|
|
98
|
+
if (!supported && parsed.kind !== 'doctor') {
|
|
99
|
+
stderr.write(`RoomSync's bundled host requires Windows x64 (detected ${platform}/${arch}). Use Windows PowerShell or Command Prompt with x64 Node.js, not WSL. No host was started.\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
let verified;
|
|
103
|
+
try { verified = verifyPackage(root); }
|
|
104
|
+
catch (error) { stderr.write(`RoomSync: ${error.message}\n`); return 1; }
|
|
105
|
+
if (parsed.kind === 'doctor') {
|
|
106
|
+
stdout.write(`RoomSync ${pkg.version}\nPlatform: ${platform}/${arch} (${supported ? 'supported' : 'unsupported'})\nBundled Windows x64 host: checksum verified\nBundle manifest version: ${verified.manifest.version}\nSource commit: ${verified.manifest.sourceCommit}\nAudio hardware/runtime validation: not performed by this check\n`);
|
|
107
|
+
return supported ? 0 : 1;
|
|
108
|
+
}
|
|
109
|
+
return runNative(verified.executable, parsed.args, { spawnImpl, processImpl, platform, stderr });
|
|
110
|
+
}
|
|
111
|
+
module.exports = { parseArgs, help, run, runNative };
|
package/lib/verify.cjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
|
|
6
|
+
const NATIVE_RELATIVE = 'native/win32-x64/roomsync-desktop.exe';
|
|
7
|
+
const MAX_BINARY_BYTES = 100 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function readRegularFile(file, maximum) {
|
|
10
|
+
const stat = fs.lstatSync(file);
|
|
11
|
+
if (!stat.isFile() || stat.size > maximum) throw new Error(`Invalid package file: ${path.basename(file)}`);
|
|
12
|
+
return fs.readFileSync(file);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function inspectWindowsX64(bytes) {
|
|
16
|
+
if (bytes.length < 64 || bytes.readUInt16LE(0) !== 0x5a4d) throw new Error('Native binary is not a Windows executable.');
|
|
17
|
+
const offset = bytes.readUInt32LE(0x3c);
|
|
18
|
+
if (offset > bytes.length - 26 || bytes.readUInt32LE(offset) !== 0x00004550 ||
|
|
19
|
+
bytes.readUInt16LE(offset + 4) !== 0x8664 || bytes.readUInt16LE(offset + 24) !== 0x20b) {
|
|
20
|
+
throw new Error('Native binary must be a Windows x64 PE32+ executable.');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function verifyPackage(root) {
|
|
25
|
+
let pkg, manifest, bytes;
|
|
26
|
+
try {
|
|
27
|
+
pkg = JSON.parse(readRegularFile(path.join(root, 'package.json'), 64 * 1024));
|
|
28
|
+
manifest = JSON.parse(readRegularFile(path.join(root, 'native/manifest.json'), 64 * 1024));
|
|
29
|
+
bytes = readRegularFile(path.join(root, NATIVE_RELATIVE), MAX_BINARY_BYTES);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error.code === 'ENOENT') throw new Error('This package is missing its bundled Windows host. Reinstall a complete RoomSync package; no executable will be downloaded automatically.');
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
if (manifest.schemaVersion !== 1 || manifest.platform !== 'win32' || manifest.arch !== 'x64' ||
|
|
35
|
+
manifest.binary !== NATIVE_RELATIVE || manifest.version !== pkg.version ||
|
|
36
|
+
typeof manifest.sourceCommit !== 'string' || !/^[a-f0-9]{40}$/.test(manifest.sourceCommit) ||
|
|
37
|
+
typeof manifest.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(manifest.sha256) ||
|
|
38
|
+
!Number.isSafeInteger(manifest.sizeBytes) || manifest.sizeBytes !== bytes.length) {
|
|
39
|
+
throw new Error('Invalid or mismatched native package manifest. Reinstall a complete RoomSync package.');
|
|
40
|
+
}
|
|
41
|
+
inspectWindowsX64(bytes);
|
|
42
|
+
if (crypto.createHash('sha256').update(bytes).digest('hex') !== manifest.sha256) {
|
|
43
|
+
throw new Error('Bundled Windows host checksum mismatch. Refusing to run it; reinstall the package.');
|
|
44
|
+
}
|
|
45
|
+
const notices = readRegularFile(path.join(root, 'THIRD_PARTY_NOTICES.txt'), 10 * 1024 * 1024);
|
|
46
|
+
if (notices.length < 100) throw new Error('Third-party license notices are missing.');
|
|
47
|
+
return { executable: path.join(root, NATIVE_RELATIVE), manifest, package: pkg };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { NATIVE_RELATIVE, MAX_BINARY_BYTES, inspectWindowsX64, verifyPackage };
|
|
51
|
+
if (require.main === module) {
|
|
52
|
+
try {
|
|
53
|
+
const result = verifyPackage(path.resolve(__dirname, '..'));
|
|
54
|
+
console.error(`Verified RoomSync ${result.package.version}: Windows x64, SHA256 ${result.manifest.sha256}`);
|
|
55
|
+
} catch (error) { console.error(`RoomSync package: ${error.message}`); process.exitCode = 1; }
|
|
56
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"platform": "win32",
|
|
5
|
+
"arch": "x64",
|
|
6
|
+
"binary": "native/win32-x64/roomsync-desktop.exe",
|
|
7
|
+
"sourceCommit": "aff5c964f0ed2f5449429e960740677093b59abf",
|
|
8
|
+
"sha256": "6c7e9107b9f8dfc662674354c55cf3895abfcd191cada673a4a66c96b470b7c3",
|
|
9
|
+
"sizeBytes": 1104384
|
|
10
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roomsync",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "RoomSync Windows host preview: start a local control page for private audio sharing",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"roomsync": "bin/roomsync.cjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"lib/",
|
|
15
|
+
"native/",
|
|
16
|
+
"README.md",
|
|
17
|
+
"THIRD_PARTY_NOTICES.txt"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test test/*.test.cjs",
|
|
21
|
+
"prepack": "node lib/verify.cjs"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Nyaughh/roomsync.git",
|
|
26
|
+
"directory": "packages/npm"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"audio",
|
|
30
|
+
"windows",
|
|
31
|
+
"wasapi",
|
|
32
|
+
"roomsync"
|
|
33
|
+
]
|
|
34
|
+
}
|