Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/cli/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,9 @@ export function hubInviteCommand(
managementUrl: string,
clients: OcxConnectedClientId[],
): string {
const clientsFlag = clients.length > 0 ? ` --clients ${clients.join(",")}` : "";
return `echo '${code}' | ocx connect ${dataUrl} --management-url ${managementUrl}${clientsFlag} --pairing-code-stdin`;
const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Generate commands for the joining machine's shell

When the joining machine uses cmd.exe, single quotes do not protect shell metacharacters, so an accepted origin such as https://x&whoami&x causes the pasted invite line to execute whoami as a separate command. This can occur even when the hub itself runs Unix because the generated command is explicitly intended for another machine; the repository's existing CMD escaper confirms that & and | require escaping in src/update/npm-invocation.mjs:4-8. Emit or label a POSIX-only command and provide safe PowerShell/CMD variants rather than treating this quoting as shell-independent.

AGENTS.md reference: src/AGENTS.md:L20-L20

Useful? React with 👍 / 👎.

const clientsFlag = clients.length > 0 ? ` --clients ${shellQuote(clients.join(","))}` : "";
return `echo ${shellQuote(code)} | ocx connect ${shellQuote(dataUrl)} --management-url ${shellQuote(managementUrl)}${clientsFlag} --pairing-code-stdin`;
Comment on lines +232 to +233

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Synchronize the documented invite command

The emitted and JSON-envelope command now quotes every URL and client argument, but docs-site/src/content/docs/guides/remote-hub.md:328-331 and :384-389 still claim to show the exact printed line while using the old unquoted form, and the Korean translation retains it as well. Update the English example and translated copies so users are not taught the unsafe command shape this change removes.

AGENTS.md reference: src/AGENTS.md:L29-L29

Useful? React with 👍 / 👎.

}

async function runInvite(args: string[], deps: HubCommandDeps): Promise<number> {
Expand Down
21 changes: 14 additions & 7 deletions tests/cli/hub-invite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,16 @@ describe("hub invite argument and origin helpers", () => {

test("the printed command carries --clients only when the operator asked for it", () => {
expect(hubInviteCommand(GRANT, "https://d.test:8443", "https://m.test", []))
.toBe(`echo '${GRANT}' | ocx connect https://d.test:8443 --management-url https://m.test --pairing-code-stdin`);
.toBe(`echo '${GRANT}' | ocx connect 'https://d.test:8443' --management-url 'https://m.test' --pairing-code-stdin`);
expect(hubInviteCommand(GRANT, "https://d.test:8443", "https://m.test", ["codex"]))
.toContain("--clients codex --pairing-code-stdin");
.toContain("--clients 'codex' --pairing-code-stdin");
});

test("shell-quotes every dynamic value in the printed command", () => {
expect(hubInviteCommand("code'word", "https://x$({touch,pwn})", "https://m.test/$HOME", ["codex"]))
.toBe(
`echo 'code'"'"'word' | ocx connect 'https://x$({touch,pwn})' --management-url 'https://m.test/$HOME' --clients 'codex' --pairing-code-stdin`,
);
});
});

Expand All @@ -192,7 +199,7 @@ describe("hub invite output", () => {
expect(boundOrigin).toBe("http://localhost:10100");
expect(out.join("\n")).toContain("# Run on the other machine:");
expect(out.join("\n")).toContain(
`echo '${GRANT}' | ocx connect http://100.64.0.10:10100 --management-url https://hub.tailnet.ts.net --pairing-code-stdin`,
`echo '${GRANT}' | ocx connect 'http://100.64.0.10:10100' --management-url 'https://hub.tailnet.ts.net' --pairing-code-stdin`,
);
// The warning is advice, not output a script should capture.
expect(err.join("\n")).toContain("single-use");
Expand Down Expand Up @@ -222,10 +229,10 @@ describe("hub invite output", () => {
hub: { managementPublicOrigin: "https://hub.tailnet.ts.net", dataPublicOrigin: "https://hub.tailnet.ts.net:8443" },
});
const fromConfig = await invite(["invite"], configured);
expect(fromConfig.out.join("\n")).toContain("ocx connect https://hub.tailnet.ts.net:8443 ");
expect(fromConfig.out.join("\n")).toContain("ocx connect 'https://hub.tailnet.ts.net:8443' ");

const overridden = await invite(["invite", "--data-url", "https://front.test"], configured);
expect(overridden.out.join("\n")).toContain("ocx connect https://front.test ");
expect(overridden.out.join("\n")).toContain("ocx connect 'https://front.test' ");
});

test("--json emits exactly the documented envelope", async () => {
Expand All @@ -236,7 +243,7 @@ describe("hub invite output", () => {
expiresAt: new Date(EXPIRES_AT).toISOString(),
dataUrl: "http://100.64.0.10:10100",
managementUrl: "https://hub.tailnet.ts.net",
command: `echo '${GRANT}' | ocx connect http://100.64.0.10:10100 --management-url https://hub.tailnet.ts.net --clients codex,claude --pairing-code-stdin`,
command: `echo '${GRANT}' | ocx connect 'http://100.64.0.10:10100' --management-url 'https://hub.tailnet.ts.net' --clients 'codex,claude' --pairing-code-stdin`,
});
});
});
Expand Down Expand Up @@ -321,7 +328,7 @@ describe("hub invite refuses before burning a code", () => {
hub: { managementPublicOrigin: "https://hub.tailnet.ts.net", dataPublicOrigin: "https://hub.tailnet.ts.net:8443" },
}), { live: { pid: 4242, port: 10100, hostname: "127.0.0.1", source: "runtime" } });
expect(viaConfig.code).toBe(0);
expect(viaConfig.out.join("\n")).toContain("ocx connect https://hub.tailnet.ts.net:8443 ");
expect(viaConfig.out.join("\n")).toContain("ocx connect 'https://hub.tailnet.ts.net:8443' ");
});

test("no running hub, a malformed origin, and a refused mint each exit 1 with a reason", async () => {
Expand Down
Loading