@sksoftofficial/ocduet 0.2.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sksoftofficial/ocduet",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Duet for opencode: pair your phone with a local opencode TUI. Type on mobile, see it live on both screens. LAN-first, QR pairing, zero config.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -88,11 +88,40 @@ function unitFlag(argv, name) {
88
88
  return i !== -1 && argv[i + 1] ? argv[i + 1] : null;
89
89
  }
90
90
 
91
+ const SYS_UNIT_PATH = "/etc/systemd/system/ocduet-relayer.service";
92
+
93
+ function shellQuote(a) {
94
+ return /^[\w\/.:=-]+$/.test(a) ? a : JSON.stringify(a);
95
+ }
96
+
97
+ function unitText(entry, root, dir, serveArgs, system) {
98
+ return [
99
+ "[Unit]",
100
+ "Description=ocduet relayer — public tunnel for the ocduet daemon",
101
+ "After=network-online.target",
102
+ "Wants=network-online.target",
103
+ "",
104
+ "[Service]",
105
+ `WorkingDirectory=${root}`,
106
+ `ExecStart=${[process.execPath, ...serveArgs].map(shellQuote).join(" ")}`,
107
+ `Environment=OCDUET_RELAYER_DIR=${dir}`,
108
+ "Restart=always",
109
+ "RestartSec=3",
110
+ "",
111
+ "[Install]",
112
+ `WantedBy=${system ? "multi-user.target" : "default.target"}`,
113
+ "",
114
+ ].join("\n");
115
+ }
116
+
91
117
  // `ocduet install relayer` — set the relayer up as a boot-persistent service
92
- // on a public box. systemd user unit when available (enable-linger keeps it
93
- // alive without a login session), detached process + pid file otherwise.
118
+ // on a public box, no manual steps. Prefers a system systemd unit (the usual
119
+ // VPS case: root over ssh, or passwordless sudo), then a systemd user unit
120
+ // (enable-linger keeps it alive without a login session), and finally a
121
+ // detached process + pid file.
94
122
  export async function installRelayer(argv = []) {
95
123
  const root = pkgRoot();
124
+ const entry = path.join(root, "bin/ocduet.js");
96
125
  const dir = relayerDir();
97
126
  const port = unitFlag(argv, "--port") || process.env.OCDUET_RELAYER_PORT || "4290";
98
127
  const cert = unitFlag(argv, "--cert");
@@ -100,44 +129,55 @@ export async function installRelayer(argv = []) {
100
129
  fs.mkdirSync(dir, { recursive: true });
101
130
 
102
131
  // make sure the store (and the enroll secret) exists before we print it
103
- const secret = spawnSync(process.execPath, [path.join(root, "bin/ocduet.js"), "relay", "secret"], { encoding: "utf8", env: { ...process.env, OCDUET_RELAYER_DIR: dir } });
132
+ const secret = spawnSync(process.execPath, [entry, "relay", "secret"], { encoding: "utf8", env: { ...process.env, OCDUET_RELAYER_DIR: dir } });
104
133
  const enroll = (secret.stdout || "").trim() || "(unknown — run `ocduet relayer secret`)";
105
134
 
106
- const serveArgs = ["ocduet.js", "relayer", "serve", "--port", port];
135
+ const serveArgs = [entry, "relay", "serve", "--port", port];
107
136
  if (cert && key) serveArgs.push("--cert", cert, "--key", key);
108
137
 
138
+ const hasSystemd = fs.existsSync("/run/systemd/system");
139
+ const euid = typeof process.geteuid === "function" ? process.geteuid() : -1;
140
+ const canSudo = () => spawnSync("sudo", ["-n", "true"]).status === 0;
141
+
109
142
  let managedBy = null;
110
- const unitDir = path.join(os.homedir(), ".config/systemd/user");
111
- const unitPath = path.join(unitDir, "ocduet-relayer.service");
112
- try {
113
- fs.mkdirSync(unitDir, { recursive: true });
114
- fs.writeFileSync(unitPath, [
115
- "[Unit]",
116
- "Description=ocduet relayer public tunnel for the ocduet daemon",
117
- "After=network-online.target",
118
- "Wants=network-online.target",
119
- "",
120
- "[Service]",
121
- `WorkingDirectory=${root}`,
122
- `ExecStart=${process.execPath} ${serveArgs.map((a) => a.includes(" ") ? JSON.stringify(a) : a).join(" ")}`,
123
- "Environment=OCDUET_RELAYER_DIR=" + dir,
124
- "Restart=always",
125
- "RestartSec=3",
126
- "",
127
- "[Install]",
128
- "WantedBy=default.target",
129
- "",
130
- ].join("\n"));
131
- const ctl = spawnSync("systemctl", ["--user", "daemon-reload"], { encoding: "utf8" });
132
- if (ctl.status === 0) {
133
- spawnSync("systemctl", ["--user", "enable", "--now", "ocduet-relayer.service"], { encoding: "utf8" });
134
- managedBy = "systemd --user";
143
+ if (hasSystemd && (euid === 0 || canSudo())) {
144
+ // root, or passwordless sudo: real system unit — starts now, survives reboots
145
+ const unit = unitText(entry, root, dir, serveArgs, true);
146
+ const wrote = euid === 0
147
+ ? (() => { try { fs.writeFileSync(SYS_UNIT_PATH, unit); return true; } catch { return false; } })()
148
+ : spawnSync("sudo", ["-n", "tee", SYS_UNIT_PATH], { input: unit, encoding: "utf8" }).status === 0;
149
+ const sys = (args) => euid === 0
150
+ ? spawnSync("systemctl", args, { encoding: "utf8" })
151
+ : spawnSync("sudo", ["-n", "systemctl", ...args], { encoding: "utf8" });
152
+ if (wrote && sys(["daemon-reload"]).status === 0) {
153
+ sys(["enable", "--now", "ocduet-relayer.service"]);
154
+ sys(["restart", "ocduet-relayer.service"]); // pick up unit changes on re-install
155
+ if (sys(["is-active", "--quiet", "ocduet-relayer.service"]).status === 0) {
156
+ managedBy = `systemd system unit (${SYS_UNIT_PATH})`;
157
+ }
135
158
  }
136
- } catch {}
159
+ }
160
+
161
+ if (!managedBy) {
162
+ const unitDir = path.join(os.homedir(), ".config/systemd/user");
163
+ const unitPath = path.join(unitDir, "ocduet-relayer.service");
164
+ try {
165
+ fs.mkdirSync(unitDir, { recursive: true });
166
+ fs.writeFileSync(unitPath, unitText(entry, root, dir, serveArgs, false));
167
+ const ctl = spawnSync("systemctl", ["--user", "daemon-reload"], { encoding: "utf8" });
168
+ if (ctl.status === 0) {
169
+ spawnSync("systemctl", ["--user", "enable", "--now", "ocduet-relayer.service"], { encoding: "utf8" });
170
+ spawnSync("systemctl", ["--user", "restart", "ocduet-relayer.service"], { encoding: "utf8" });
171
+ if (spawnSync("systemctl", ["--user", "is-active", "--quiet", "ocduet-relayer.service"], { encoding: "utf8" }).status === 0) {
172
+ managedBy = "systemd --user";
173
+ }
174
+ }
175
+ } catch {}
176
+ }
137
177
 
138
178
  if (!managedBy) {
139
179
  const log = fs.openSync(path.join(dir, "relayer.log"), "a", 0o600);
140
- const child = spawn(process.execPath, [path.join(root, "bin/ocduet.js"), ...serveArgs], {
180
+ const child = spawn(process.execPath, serveArgs, {
141
181
  env: { ...process.env, OCDUET_RELAYER_DIR: dir },
142
182
  detached: true,
143
183
  stdio: ["ignore", log, log],
@@ -161,7 +201,10 @@ export async function installRelayer(argv = []) {
161
201
  console.log("on your DESKTOP, link it:");
162
202
  console.log(` ocduet relay link https://${host}:${port} ${enroll}`);
163
203
  if (ips.length > 1) console.log(` (this box has multiple IPs: ${ips.join(", ")} — use the public one)`);
164
- if (managedBy.startsWith("systemd")) {
204
+ if (managedBy.startsWith("systemd system")) {
205
+ console.log("");
206
+ console.log("logs: journalctl -u ocduet-relayer -f");
207
+ } else if (managedBy === "systemd --user") {
165
208
  console.log("");
166
209
  console.log("to survive reboots without a login session, run once (may need root):");
167
210
  console.log(` sudo loginctl enable-linger $USER`);
@@ -40,7 +40,9 @@ function registerOnce(url, desktopId, pubB64, sign, secret, certPem) {
40
40
  return new Promise((resolve, reject) => {
41
41
  const wsUrl = new URL("/desktop", url);
42
42
  wsUrl.protocol = wsUrl.protocol === "http:" ? "ws:" : "wss:";
43
- const tlsOpts = certPem ? { ca: certPem } : { rejectUnauthorized: false };
43
+ // pinned cert = the identity itself; hostname/SAN matching would always
44
+ // fail for a bare-IP relayer with a self-signed cert
45
+ const tlsOpts = certPem ? { ca: certPem, checkServerIdentity: () => undefined } : { rejectUnauthorized: false };
44
46
  const ws = new WebSocket(wsUrl, { ...tlsOpts, handshakeTimeout: 10_000 });
45
47
  const fail = (why) => { try { ws.close(); } catch {} reject(new Error(why)); };
46
48
  const timer = setTimeout(() => fail("register timeout"), 12_000);
@@ -33,7 +33,9 @@ export function readRelayConfig() {
33
33
  // auth still comes from the ed25519 register signature — the pin kills
34
34
  // silent MITM even without a domain.
35
35
  export function relayTlsOpts(cfg) {
36
- return cfg?.certPem ? { ca: cfg.certPem } : { rejectUnauthorized: false };
36
+ // when pinned, the exact cert IS the identity skip hostname/SAN matching,
37
+ // which can never pass for a bare-IP host anyway
38
+ return cfg?.certPem ? { ca: cfg.certPem, checkServerIdentity: () => undefined } : { rejectUnauthorized: false };
37
39
  }
38
40
 
39
41
  const state = { linked: false, connected: false, url: null, desktopId: null, since: null, lastError: null, reconnects: 0 };
package/src/relayer.js CHANGED
@@ -75,11 +75,17 @@ function ensureCert() {
75
75
  return { cert: fs.readFileSync(certP), key: fs.readFileSync(keyP) };
76
76
  }
77
77
  fs.mkdirSync(relayerDir(), { recursive: true });
78
+ // cover every address the box might be reached on — desktops link by bare IP
79
+ const ips = [];
80
+ for (const addrs of Object.values(os.networkInterfaces())) {
81
+ for (const a of addrs || []) if (a.family === "IPv4" && !a.internal) ips.push(a.address);
82
+ }
83
+ const san = ["DNS:localhost", "IP:127.0.0.1", ...ips.map((ip) => `IP:${ip}`)];
78
84
  const out = spawnSync("openssl", [
79
85
  "req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1",
80
86
  "-keyout", keyP, "-out", certP, "-days", "3650", "-nodes",
81
87
  "-subj", "/CN=ocduet-relayer",
82
- "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1",
88
+ "-addext", `subjectAltName=${san.join(",")}`,
83
89
  ], { stdio: "ignore" });
84
90
  if (out.status !== 0) throw new Error("openssl cert generation failed — is openssl installed?");
85
91
  try { fs.chmodSync(keyP, 0o600); } catch {}