@chendesheng/comm100-auto-login 0.0.1 → 0.0.3
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 +13 -4
- package/extension/manifest.json +1 -1
- package/package.json +14 -4
- package/src/cli/accounts.mjs +2 -1
- package/src/cli/bridge.mjs +5 -3
- package/src/cli/native-host.mjs +16 -12
- package/src/cli/setup.mjs +37 -8
package/README.md
CHANGED
|
@@ -43,10 +43,19 @@ comm100-accounts setup
|
|
|
43
43
|
Setup registers a [Chrome native messaging host](https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging)
|
|
44
44
|
for this extension and profile. It uses one temporary LevelDB snapshot to discover the profile's
|
|
45
45
|
connection ID; it never opens Chrome's original database. The launcher uses this installation's
|
|
46
|
-
absolute Node.js path, so Chrome does not need Node on its PATH. Setup
|
|
47
|
-
macOS and Linux. Chrome must be running, with the extension enabled, for subsequent CLI commands.
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
absolute Node.js path, so Chrome does not need Node on its PATH. Setup supports Chrome on Windows,
|
|
47
|
+
macOS, and Linux. Chrome must be running, with the extension enabled, for subsequent CLI commands.
|
|
48
|
+
|
|
49
|
+
On Windows, setup installs the launcher and configuration in `%LOCALAPPDATA%\comm100-accounts`
|
|
50
|
+
and registers the native host under
|
|
51
|
+
`HKCU\Software\Google\Chrome\NativeMessagingHosts\com.comm100.accounts` for the current user;
|
|
52
|
+
administrator access is not required. Communication uses a Windows named pipe. The same install,
|
|
53
|
+
`extension-path`, and `setup` commands work in PowerShell and Command Prompt. If PowerShell blocks
|
|
54
|
+
npm's script launcher, use `npm.cmd` and `comm100-accounts.cmd`. After a Node.js installation moves,
|
|
55
|
+
run setup again so the launcher uses its new path.
|
|
56
|
+
|
|
57
|
+
All normal reads and writes go through the extension service worker over local IPC (a Windows
|
|
58
|
+
named pipe or a Unix socket).
|
|
50
59
|
Accounts and JWTs share the extension's `chrome.storage.local` state; no database snapshots are
|
|
51
60
|
made for `list`, `login`, or `tokens`, and there is no separate JWT cache file. Bridge configuration
|
|
52
61
|
contains only profile/connection metadata, not credentials or tokens.
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Comm100 Auto Login",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"description": "Silently signs in to Comm100 test dashboards with saved credentials, preferring the most recently used account.",
|
|
6
6
|
"minimum_chrome_version": "116",
|
|
7
7
|
"permissions": [
|
package/package.json
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chendesheng/comm100-auto-login",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Comm100 auto-login extension and account CLI. The extension has no runtime dependencies; the CLI uses classic-level.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/chendesheng/Comm100Login.git"
|
|
9
9
|
},
|
|
10
|
-
"publishConfig": {
|
|
11
|
-
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"src/cli/",
|
|
16
|
+
"src/shared/",
|
|
17
|
+
"extension/",
|
|
18
|
+
"scripts/postinstall.mjs"
|
|
19
|
+
],
|
|
12
20
|
"bin": {
|
|
13
21
|
"comm100-accounts": "bin/comm100-accounts.mjs"
|
|
14
22
|
},
|
|
15
|
-
"engines": {
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
16
26
|
"scripts": {
|
|
17
27
|
"build:extension": "node scripts/build-extension.mjs",
|
|
18
28
|
"prepack": "npm run build:extension",
|
package/src/cli/accounts.mjs
CHANGED
|
@@ -91,7 +91,8 @@ export async function readChromeState(source) {
|
|
|
91
91
|
let db;
|
|
92
92
|
try {
|
|
93
93
|
const snapshot = join(temporary, 'db');
|
|
94
|
-
|
|
94
|
+
// Chrome holds LOCK exclusively on Windows. The snapshot creates its own lock.
|
|
95
|
+
await cp(source, snapshot, { recursive: true, filter: (path) => basename(path) !== 'LOCK' });
|
|
95
96
|
db = new ClassicLevel(snapshot, { keyEncoding: 'utf8', valueEncoding: 'json', createIfMissing: false });
|
|
96
97
|
const raw = await db.get('comm100AutoLogin');
|
|
97
98
|
if (!raw || !Array.isArray(raw.accounts)) throw new Error('The database has no Comm100 account list.');
|
package/src/cli/bridge.mjs
CHANGED
|
@@ -35,12 +35,12 @@ export function messageReader(onMessage, onError) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function socketDirectory() {
|
|
38
|
-
if (platform() === 'win32') throw new Error('The native CLI bridge currently supports macOS and Linux.');
|
|
39
38
|
return `/tmp/comm100-accounts-${process.getuid()}`;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
export function socketPath(bridgeId) {
|
|
41
|
+
export function socketPath(bridgeId, os = platform()) {
|
|
43
42
|
if (!/^[a-f0-9-]{36}$/i.test(bridgeId || '')) throw new Error('Reload the updated extension, then run comm100-accounts setup.');
|
|
43
|
+
if (os === 'win32') return `\\\\.\\pipe\\comm100-accounts-${bridgeId}`;
|
|
44
44
|
return join(socketDirectory(), `${bridgeId}.sock`);
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -50,7 +50,9 @@ export function chromeDataDirectory() {
|
|
|
50
50
|
return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'google-chrome');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export const bridgeConfigDirectory = () => join(
|
|
53
|
+
export const bridgeConfigDirectory = () => join(platform() === 'win32'
|
|
54
|
+
? process.env.LOCALAPPDATA || join(homedir(), 'AppData/Local')
|
|
55
|
+
: process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'comm100-accounts');
|
|
54
56
|
export const bridgeConfigFile = () => join(bridgeConfigDirectory(), 'bridge.json');
|
|
55
57
|
|
|
56
58
|
export async function readBridgeConfig(filename = bridgeConfigFile()) {
|
package/src/cli/native-host.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createConnection, createServer } from 'node:net';
|
|
2
2
|
import { chmod, lstat, mkdir, rm } from 'node:fs/promises';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { platform } from 'node:os';
|
|
4
5
|
import { encodeMessage, messageReader, socketDirectory, socketPath } from './bridge.mjs';
|
|
5
6
|
|
|
6
7
|
/** Chrome owns stdio; the local socket carries CLI requests to that same native port. */
|
|
7
8
|
export async function startNativeHost({ input = process.stdin, output = process.stdout } = {}) {
|
|
9
|
+
const windows = platform() === 'win32';
|
|
8
10
|
const clients = new Set();
|
|
9
11
|
const pending = new Map();
|
|
10
12
|
let server, path, closed = false;
|
|
@@ -14,24 +16,24 @@ export async function startNativeHost({ input = process.stdin, output = process.
|
|
|
14
16
|
closed = true;
|
|
15
17
|
for (const socket of clients) socket.destroy();
|
|
16
18
|
if (server?.listening) await new Promise((done) => server.close(done));
|
|
17
|
-
if (path) await rm(path, { force: true });
|
|
19
|
+
if (path && !windows) await rm(path, { force: true });
|
|
18
20
|
}
|
|
19
21
|
async function listen(bridgeId) {
|
|
20
|
-
const directory = socketDirectory();
|
|
21
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
22
|
-
const details = await lstat(directory);
|
|
23
|
-
if (!details.isDirectory() || details.uid !== process.getuid()) throw new Error('Invalid CLI bridge socket directory.');
|
|
24
|
-
await chmod(directory, 0o700);
|
|
25
22
|
const target = socketPath(bridgeId);
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
if (!windows) {
|
|
24
|
+
const directory = socketDirectory();
|
|
25
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
26
|
+
const details = await lstat(directory);
|
|
27
|
+
if (!details.isDirectory() || details.uid !== process.getuid()) throw new Error('Invalid CLI bridge socket directory.');
|
|
28
|
+
await chmod(directory, 0o700);
|
|
29
|
+
// Remove only an abandoned socket. Never displace a live Chrome/profile connection.
|
|
28
30
|
await new Promise((accept, reject) => {
|
|
29
31
|
const probe = createConnection(target);
|
|
30
32
|
probe.on('connect', () => { probe.destroy(); reject(new Error('CLI bridge is already connected for this profile.')); });
|
|
31
33
|
probe.on('error', (error) => ['ENOENT', 'ECONNREFUSED'].includes(error.code) ? accept() : reject(error));
|
|
32
34
|
});
|
|
33
35
|
await rm(target, { force: true });
|
|
34
|
-
}
|
|
36
|
+
}
|
|
35
37
|
if (closed) return;
|
|
36
38
|
server = createServer((socket) => {
|
|
37
39
|
clients.add(socket);
|
|
@@ -49,11 +51,13 @@ export async function startNativeHost({ input = process.stdin, output = process.
|
|
|
49
51
|
});
|
|
50
52
|
await new Promise((accept, reject) => {
|
|
51
53
|
server.once('error', reject);
|
|
52
|
-
|
|
54
|
+
// Keep libuv's default access restrictions; never grant access to all users.
|
|
55
|
+
// Windows owns named-pipe lifetime and prevents a second server binding it.
|
|
56
|
+
server.listen({ path: target, readableAll: false, writableAll: false }, accept);
|
|
53
57
|
});
|
|
54
58
|
path = target;
|
|
55
|
-
if (closed) { await rm(path, { force: true }); server.close(); return; }
|
|
56
|
-
await chmod(path, 0o600);
|
|
59
|
+
if (closed) { if (!windows) await rm(path, { force: true }); server.close(); return; }
|
|
60
|
+
if (!windows) await chmod(path, 0o600);
|
|
57
61
|
send({ type: 'listening' });
|
|
58
62
|
}
|
|
59
63
|
let initializing = false;
|
package/src/cli/setup.mjs
CHANGED
|
@@ -2,15 +2,31 @@ import { chmod, mkdir, rename, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, dirname, join } from 'node:path';
|
|
3
3
|
import { platform } from 'node:os';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
5
7
|
import { bridgeConfigDirectory, chromeDataDirectory, NATIVE_HOST, readBridgeConfig, socketPath } from './bridge.mjs';
|
|
6
8
|
|
|
7
9
|
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
10
|
+
const execute = promisify(execFile);
|
|
8
11
|
|
|
9
|
-
export
|
|
10
|
-
|
|
12
|
+
export function windowsLauncher(node, script) {
|
|
13
|
+
const quote = (value) => {
|
|
14
|
+
if (/["\r\n\0]/.test(value)) throw new Error('Invalid native host launcher path.');
|
|
15
|
+
return `"${value.replaceAll('%', '%%')}"`;
|
|
16
|
+
};
|
|
17
|
+
// stdout must contain only binary native-messaging frames. Disable expansion of !
|
|
18
|
+
// in paths, escape literal %, and use UTF-8 for non-ASCII installation directories.
|
|
19
|
+
return `@echo off\r\nsetlocal DisableDelayedExpansion\r\nchcp 65001 >nul\r\n${quote(node)} ${quote(script)}\r\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function installBridge(database, state, {
|
|
23
|
+
configDirectory = bridgeConfigDirectory(), chromeDirectory = chromeDataDirectory(),
|
|
24
|
+
os = platform(), runCommand = execute
|
|
25
|
+
} = {}) {
|
|
26
|
+
const windows = os === 'win32';
|
|
11
27
|
const extensionId = basename(database);
|
|
12
28
|
if (!/^[a-p]{32}$/.test(extensionId)) throw new Error('--db must identify the installed Chrome extension directory for setup.');
|
|
13
|
-
socketPath(state.cliBridgeId); // Validate before writing any setup files.
|
|
29
|
+
socketPath(state.cliBridgeId, os); // Validate before writing any setup files.
|
|
14
30
|
const configFile = join(configDirectory, 'bridge.json');
|
|
15
31
|
const config = await readBridgeConfig(configFile);
|
|
16
32
|
const profilePath = dirname(dirname(database));
|
|
@@ -18,17 +34,30 @@ export async function installBridge(database, state, { configDirectory = bridgeC
|
|
|
18
34
|
connections.push({ profilePath, database, extensionId, bridgeId: state.cliBridgeId });
|
|
19
35
|
const directory = configDirectory;
|
|
20
36
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
21
|
-
const launcher = join(directory, 'native-host');
|
|
37
|
+
const launcher = join(directory, windows ? 'native-host.bat' : 'native-host');
|
|
22
38
|
const script = fileURLToPath(new URL('../../bin/comm100-accounts-host.mjs', import.meta.url));
|
|
23
|
-
await writeFile(launcher,
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
await writeFile(launcher, windows ? windowsLauncher(process.execPath, script)
|
|
40
|
+
: `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(script)} "$@"\n`, { mode: 0o700 });
|
|
41
|
+
if (!windows) await chmod(launcher, 0o700);
|
|
42
|
+
const manifestDirectory = windows ? directory : join(chromeDirectory, 'NativeMessagingHosts');
|
|
26
43
|
await mkdir(manifestDirectory, { recursive: true });
|
|
27
44
|
const manifest = {
|
|
28
45
|
name: NATIVE_HOST, description: 'Comm100 account CLI bridge', path: launcher, type: 'stdio',
|
|
29
46
|
allowed_origins: [...new Set(connections.map((entry) => `chrome-extension://${entry.extensionId}/`))]
|
|
30
47
|
};
|
|
31
|
-
|
|
48
|
+
const manifestPath = join(manifestDirectory, `${NATIVE_HOST}.json`);
|
|
49
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
50
|
+
if (windows) {
|
|
51
|
+
// Chrome checks the 32-bit registry view first, even on 64-bit Windows.
|
|
52
|
+
// HKCU needs no administrator privileges; execFile avoids shell interpolation.
|
|
53
|
+
const registry = join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'reg.exe');
|
|
54
|
+
try {
|
|
55
|
+
await runCommand(registry, ['ADD', `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${NATIVE_HOST}`,
|
|
56
|
+
'/ve', '/t', 'REG_SZ', '/d', manifestPath, '/f', '/reg:32'], { windowsHide: true });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error('Cannot register the Chrome native host for this Windows user. Run comm100-accounts setup again.', { cause: error });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
32
61
|
const temporary = `${configFile}.${process.pid}.tmp`;
|
|
33
62
|
await writeFile(temporary, JSON.stringify({ connections }, null, 2) + '\n', { mode: 0o600 });
|
|
34
63
|
await rename(temporary, configFile);
|